【免费下载链接】oat
Ultra-lightweight, zero dependency, semantic HTML, CSS, JS UI library. ~10KB min+gz.
Oat(oat)是一套超轻量、零依赖的语义化 HTML/CSS/JS UI 库,而<ot-dropdown>是其中少数由 Web Component 驱动的交互组件之一。它完全建立在浏览器原生 Popover API 之上,用最少的标记实现菜单(Menu)与弹层(Popover)两种形态,并自带视口翻转定位、键盘导航与 ARIA 状态管理。读完本文,你将掌握在 Oat 中编写可用的下拉菜单与确认弹层的完整写法,理解其底层定位、焦点与事件机制的源码实现,并能将其与按钮组等组件组合成 Split Button 等实战控件。
组件概览:语义标记 + 原生 Popover API
<ot-dropdown>的设计思路非常直接:触发器(trigger)与目标(target)之间的开合关系完全交给浏览器原生的 Popover API,Web Component 只负责定位、键盘导航和可访问性状态。
官方文档(dropdown.md)给出了三条核心规则:
- 用
<ot-dropdown>包裹整个下拉区域; - 在触发器元素上声明
popovertarget,在目标元素上声明popover; - 如果下拉内容是一个
<menu>,其中的每一项使用role="menuitem"。
也就是说,你写的仍然是标准 HTML——popovertarget、popover、role="menuitem"都是原生属性,组件本身不引入任何自定义配置项或命令式 API。
在使用组件前,需要先引入 Oat 的基础文件。按 usage.md 的说明,你可以通过 CDN 直接引入:
<link rel="stylesheet" href="https://unpkg.com/@knadh/oat/oat.min.css"> <script src="https://unpkg.com/@knadh/oat/oat.min.js" defer></script>也可以按需只引入 Dropdown 所依赖的最小文件集(参见 customizing.md 的“Picking and choosing”):
00-base.css(基础样式层,其中包含对[popover]顶层元素的字体与颜色声明)01-theme.css(全部 CSS 变量)base.js(OtBase基类)dropdown.js(本组件实现,或在完整入口 index.js 中一并引入)
组件在 dropdown.js 末尾通过customElements.define('ot-dropdown', OtDropdown)注册,引入后即可直接使用。
基础用法:菜单型 Dropdown
官方文档提供了第一个完整示例——一个包含常规操作、危险操作与链接项的“Options”菜单。下面直接继承并展开这段代码:
<ot-dropdown> <button popovertarget="demo-menu" class="outline"> Options <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg> </button> <menu popover id="demo-menu"> <button role="menuitem" class="ghost">Profile</button> <button role="menuitem" class="ghost" popovertarget="demo-menu" popovertargetaction="hide">Click to close</button> <button role="menuitem" class="ghost">Help</button> <a href="#" role="menuitem" class="unstyled">Link</a> <hr> <button role="menuitem" class="ghost">Logout</button> <button role="menuitem"><ot-dropdown> <button popovertarget="demo-confirm" class="outline"> Confirm </button> <article class="card" popover id="demo-confirm"> <header> <h4>Are you sure?</h4> <p>This action cannot be undone.</p> </header> <br /> <footer> <button class="outline small" popovertarget="demo-confirm">Cancel</button> <button>init() { this.#menu = this.querySelector('[popover]'); this.#trigger = this.querySelector('[popovertarget]'); if (!this.#menu || !this.#trigger) return; this.#menu.addEventListener('toggle', this); this.#menu.addEventListener('keydown', this); ... }init()的调用时机由基类 base.js 保证:connectedCallback()会等待DOMContentLoaded(若文档仍在加载)后再执行且只执行一次#setup()→init()。事件监听全部以this(元素本身)作为处理器传入,OtBase.handleEvent()会把事件按类型分发到onclick、onkeydown、ontoggle等以on${event.type}命名的实例方法上——这是整个 Oat 组件体系的统一事件模式。
手动定位与视口翻转
popover定位行为类似position: fixed,相对视口而言,因此组件必须在打开时手动计算坐标,源码注释直言这一点:
this.#position = () => { const r = this.#trigger.getBoundingClientRect(); const m = this.#menu.getBoundingClientRect(); // Flip if menu overflows viewport. this.#menu.style.top = `${r.bottom + m.height > window.innerHeight ? r.top - m.height : r.bottom}px`; this.#menu.style.left = `${r.left + m.width > window.innerWidth ? r.right - m.width : r.left}px`; };定位逻辑非常朴素但有效:
- 默认把菜单锚定在触发器下方(
top = r.bottom)、左侧对齐(left = r.left); - 若下方空间不足(
r.bottom + m.height > window.innerHeight),翻转到触发器上方(top = r.top - m.height); - 若右侧溢出视口,则改为右对齐(
left = r.right - m.width)。
作为配套,dropdown.css 把[popover]声明为position: fixed; margin: 0; min-width: 12rem;,确保计算坐标时不受默认边距干扰。
开合状态、焦点与滚动监听
ontoggle(e)是组件的状态中枢,根据toggle事件的newState分流:
ontoggle(e) { if (e.newState === 'open') { this.#position(); window.addEventListener('scroll', this.#position, true); window.addEventListener('resize', this.#position); this.#items = [...this.querySelectorAll('[role="menuitem"]')]; this.#items[0]?.focus(); this.#trigger.ariaExpanded = 'true'; } else { this.cleanup(); this.#items = null; this.#trigger.ariaExpanded = 'false'; this.#trigger.focus(); } }打开时:
- 立即计算并应用位置;
- 在 window 上注册
scroll(捕获阶段,true,以便捕获任意容器内的滚动)与resize监听,菜单保持跟随触发器; - 收集所有
[role="menuitem"]并聚焦第一项; - 把触发器的
aria-expanded置为"true",向辅助技术暴露展开状态。
关闭时:移除滚动/缩放监听(cleanup(),同时该方法在元素被移出 DOM 时由OtBase.disconnectedCallback()兜底调用)、清空菜单项集合、aria-expanded置为"false",并把焦点归还给触发器按钮——这保证了键盘用户的操作流不被打断。
键盘循环导航
onkeydown(e)先过滤非菜单项的按键,再把事件交给基类的keyNav计算下一个目标索引:
onkeydown(e) { if (!e.target.matches('[role="menuitem"]')) return; const idx = this.#items.indexOf(e.target); const next = this.keyNav(e, idx, this.#items.length, 'ArrowUp', 'ArrowDown', true); if (next >= 0) this.#items[next].focus(); }keyNav(base.js)实现了标准的“roving”循环导航:
ArrowDown→ 索引(idx + 1) % len;ArrowUp→ 索引(idx - 1 + len) % len(支持环绕);Home/End→ 跳到首项 / 末项(homeEnd = true时启用);- 命中任一按键都会
preventDefault(),避免页面滚动。
这套模式与 tabs.js 的ArrowLeft/ArrowRight导航同源,是整个 Oat 键盘可访问性的通用底座。
样式与主题:dropdown.css 的动画细节
dropdown.css 位于@layer components层,样式全部基于主题 CSS 变量,改动外观无需碰组件源码:
ot-dropdown { [popover] { position: fixed; margin: 0; min-width: 12rem; background-color: var(--background); border: 1px solid var(--border); border-radius: var(--radius-medium); box-shadow: var(--shadow-small); opacity: 0; transform: translateY(-4px); transition: opacity 150ms ease-out, transform 150ms ease-out, display 150ms allow-discrete, overlay 150ms allow-discrete; &:popover-open { opacity: 1; transform: translateY(0); } @starting-style { &:popover-open { opacity: 0; transform: translateY(-4px); } } ... } }值得注意的现代 CSS 特性:
@starting-style:为进入动画定义起始状态,使菜单打开时从透明、上移 4px 平滑过渡到不透明、归位;allow-discrete:允许display与overlay(顶层渲染)这类离散属性参与过渡,配合:popover-open伪类实现真正可动画的开合;prefers-reduced-motion:全局动画缩减由 animations.css 统一处理,会把过渡时长压到 0.01ms,尊重用户的系统设置。
菜单项样式同样集中在组件层:
[role="menuitem"] { display: flex; width: 100%; padding: var(--space-2) var(--space-3); justify-content: start; cursor: pointer; &:hover, &:focus { background-color: var(--accent); outline: none; } }菜单项全宽、左对齐,悬停/聚焦时以--accent高亮;outline: none由组件接管视觉焦点反馈(配合00-base.css中:focus-visible的--ring描边规则)。
若要调整下拉的圆角、阴影、间距或高亮色,只需在引入 Oat 之后覆盖对应变量,例如:
:root { --radius-medium: 0.5rem; --shadow-small: 0 4px 12px rgb(0 0 0 / 0.08); --accent: #eef2ff; }完整变量清单见 customizing.md 与 01-theme.css。
实战组合:Split Button 与真实站点用法
<ot-dropdown>的价值更多体现在与其他 Oat 组件的组合上。
Split Button(分割按钮)
recipes.md 给出了一个典型配方:用menu.buttons(Oat 的连接按钮组,见 button.css)承载主动作,用ot-dropdown承载次级动作:
<ot-dropdown> <menu class="buttons"> <li><button class="outline">Save</button></li> <li> <button class="outline" popovertarget="save-actions" aria-label="More save actions"> More <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg> </button> </li> </menu> <menu popover id="save-actions"> <button role="menuitem" class="ghost">Save draft</button> <button role="menuitem" class="ghost">Save and publish</button> <button role="menuitem" class="ghost">Duplicate</button> </menu> </ot-dropdown>要点:popovertarget声明在menu.buttons内部的按钮上,而ot-dropdown的querySelector('[popovertarget]')会命中它;menu同时复用“连接按钮组”与“下拉菜单容器”两种角色,标记量极小。
官方文档站自身的导航菜单
base.html 中,Oat 文档站的顶部导航“More”就是一个真实的<ot-dropdown>实例——包含指向 Usage、Customizing、Other libs、Recipes、Extensions 的链接项,并用<hr>分组。这证明组件可以直接服务于站点级导航,是经过实际使用的模式。
无障碍与兼容性注意
- 语义与状态:
role="menuitem"、触发器aria-expanded的动态同步、打开时聚焦首项、关闭后焦点归还触发器,构成了完整的菜单式 ARIA 交互(dropdown.js)。 - 原生能力优先:开合用
popover/popovertarget/popovertargetaction原生属性,关闭由浏览器 light-dismiss(点击外部、Esc)兜底,代码量被压到极低。 - 浏览器前提:组件依赖浏览器原生 Popover API(
[popover]属性、toggle事件),因此需要较新的浏览器版本。Oat 在 base.js 中为 Safari 的commandfor/command提供了事件委托式 polyfill,并在 tooltip.js 中用'showPopover' in HTMLElement.prototype做特性检测;而dropdown.js未做特性检测,直接假定 Popover API 可用——部署前请确认目标环境支持(主流 Chromium 与 Firefox 已支持,Safari 支持情况请以目标版本实测为准)。 - 嵌套定位:由于定位基于
getBoundingClientRect()且监听scroll/resize,当触发器位于可滚动容器内时,菜单会在捕获阶段随滚动实时重定位,一般无需额外处理。
小结
<ot-dropdown>是 Oat“少即是多”理念的典型代表:交互状态交给原生 Popover API,Web Component 只做三件事——视口内翻转定位、键盘循环导航、ARIA 状态管理,全部逻辑集中在 dropdown.js 七十余行之内。菜单与确认弹层两种形态共用同一组件,配合按钮变体(outline/ghost/danger)与主题变量即可快速产出可用、可访问、零依赖的下拉交互。
【免费下载链接】oat
Ultra-lightweight, zero dependency, semantic HTML, CSS, JS UI library. ~10KB min+gz.
相关推荐
Makepad DropDown 下拉菜单组件:属性、样式与 PopupMenu 弹层完全指南
Makepad DropDown 下拉菜单组件:属性、样式与 PopupMenu 弹层完全指南 DropDown 是 Makepad 创意软件开发平台内置的下拉
前端UI组件3D渲染跨平台游戏开发Element Plus弹出层组件:Popover、Tooltip、Dropdown精讲
Element Plus弹出层组件:Popover、Tooltip、Dropdown精讲 引言:为什么需要专业的弹出层组件? 在现代Web应用开发中,弹出层(O
前端UI组件Element UI弹出层组件:Popover、Tooltip、Popconfirm
Element UI弹出层组件:Popover、Tooltip、Popconfirm Element UI作为基于Vue.js 2.0的Web UI工具包,提供
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考