
Radix Vue Drawer 组件设计全解析手势滑动、吸附点与嵌套抽屉的完整实现【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vueDrawer抽屉是一个从屏幕边缘滑入的面板组件在移动端和响应式布局中被广泛用于底部操作单、侧边导航与快捷设置面板。本篇文章以 Radix Vue 仓库中 Drawer 设计规格文档 为主体结合 Drawer 组件源码 与其配套文档 drawer.md系统讲解这一组件的完整设计从组件拆分、上下文契约、手势引擎到吸附点算法、嵌套抽屉与无障碍实现。读完你将理解一个生产级可滑动抽屉应当如何被分解为可组合的原子部件并掌握其底层工作原理。设计背景为什么需要一个新的 Drawer这份设计文档日期 2026-04-04对应 Reka UI issue #2515分支2515-feature-drawer记录了将 Base UI 的 Drawer 组件移植到 Vue 3 的完整设计决策。核心动因有两个取代 Vaul Vue原 Vaul Vue 仓库已不再维护需要一个新的、可持续维护的抽屉实现。功能对齐 Base UI完整覆盖 Base UI Drawer 的功能面——滑动手势swipe gestures、吸附点snap points、SwipeArea、Provider/Indent 效果以及嵌套抽屉。架构上它选择独立实现直接基于 Vue 原语FocusScope、DismissableLayer、Presence、Teleport组装而不是包一层Dialog再裁剪。手势层使用vueuse/core的useEventListener、useResizeObserver并配以一个自定义的useSwipeDismiss组合式函数。这意味着抽屉的滑动逻辑与对话框是完全解耦的独立实现可以根据手势需要自由调整事件策略与数学计算。组件结构十四个部件如何协作源码中 Drawer 位于 packages/core/src/Drawer设计文档给出了清晰的目录结构packages/core/src/Drawer/ ├── DrawerRoot.vue # Context provider open/modal/swipeDirection state ├── DrawerTrigger.vue # Primitive button that opens drawer ├── DrawerPortal.vue # Teleports content to body ├── DrawerOverlay.vue # Backdrop with swipe-progress CSS var ├── DrawerContent.vue # Presence-wrapped content ├── DrawerContentImpl.vue # FocusScope DismissableLayer gesture binding ├── DrawerHandle.vue # Visible drag handle (also gesture target) ├── DrawerSwipeArea.vue # Invisible swipe-to-open zone outside popup ├── DrawerTitle.vue # h2 label ├── DrawerDescription.vue # p description ├── DrawerClose.vue # Close button ├── DrawerProvider.vue # Global coordinator for indent effects ├── DrawerIndent.vue # App UI wrapper (scales on drawer open) ├── DrawerIndentBackground.vue # Background layer (data-active/inactive) ├── composables/ │ ├── useSwipeDismiss.ts # Core gesture composable │ └── useDrawerSnapPoints.ts # Snap point resolver ├── utils.ts # CSS var names, helpers ├── index.ts # Exports └── story/ └── Drawer.story.vue实际代码与设计基本一致且新增了一个DrawerViewport.vueBase UI API 对齐的可选滚动容器与DrawerOverlayImpl.vueOverlay 的实际渲染实现被DrawerOverlay包在Presence内。各部件职责划分DrawerRoot是所有状态的唯一来源open 状态、modal 模式、swipeDirection、snapPoints并通过 provide/inject 向所有子组件分发上下文。DrawerTrigger是打开抽屉的按钮原语。DrawerPortal将内容传送到body下脱离层叠上下文。DrawerOverlay是遮罩层仅在 modal 模式下渲染并通过 CSS 变量暴露滑动进度。DrawerContent用Presence包裹负责进入/退出动画时的挂载管理DrawerContentImpl才是真正绑定FocusScope、DismissableLayer和手势事件的核心。DrawerHandle是可见的拖拽把手同时也是手势的目标区域。DrawerSwipeArea是弹层外部不可见的边缘热区用于从屏幕边缘滑入打开抽屉。DrawerProvider/DrawerIndent/DrawerIndentBackground三者协作实现 iOS 风格的卡片堆叠缩放效果。composables/useSwipeDismiss.ts与composables/useDrawerSnapPoints.ts承载全部手势与吸附数学。上下文契约DrawerRootContext所有子组件通过injectDrawerRootContext()获取根上下文源码位于 DrawerRoot.vue由createContext(DrawerRoot)创建并且被 index.ts 公开导出方便高级用户自建部件。设计文档中的契约如下interface DrawerRootContext { // State open: ReadonlyRefboolean modal: Refboolean swipeDirection: RefSwipeDirection // up | down | left | right snapPoints: RefDrawerSnapPoint[] | undefined activeSnapPoint: RefDrawerSnapPoint | null | undefined snapToSequentialPoints: Refboolean // Dimensions (set imperatively by DrawerContentImpl) popupHeight: Refnumber frontmostHeight: Refnumber // Nested drawer state hasNestedDrawer: Refboolean nestedSwiping: Refboolean nestedSwipeProgressStore: NestedSwipeProgressStore // pub/sub, no re-render // Actions onOpenChange: (value: boolean) void setActiveSnapPoint: (point: DrawerSnapPoint | null) void onPopupHeightChange: (height: number) void onNestedFrontmostHeightChange: (height: number) void onNestedDrawerPresenceChange: (present: boolean) void onNestedSwipingChange: (swiping: boolean) void onNestedSwipeProgressChange: (progress: number) void // Optional — only set when nested inside another DrawerRoot notifyParentFrontmostHeight?: (height: number) void notifyParentSwipingChange?: (swiping: boolean) void notifyParentSwipeProgressChange?: (progress: number) void notifyParentHasNestedDrawer?: (present: boolean) void // DOM refs triggerElement: RefHTMLElement | undefined contentElement: RefHTMLElement | undefined contentId: string titleId: string descriptionId: string }从源码看实际上下文在此基础上做了两处演进modal类型从boolean扩展为DrawerModal boolean | trap-focus新增仅捕获焦点但不屏蔽外部指针事件的中间档位。onOpenChange增加reason参数update:open事件携带DrawerOpenChangeDetails { reason }reason 取值为swipe | escape-key | outside-press | click | cancel | trigger-press | close-press见 DrawerRoot.vue另外新增notifyOpenComplete/update:openComplete在弹层进入/退出过渡真正结束后transitionend/animationend才触发供需要精确动画时机的消费方使用。上下文新增nestedOpenDrawerCount: Refnumber与isSwiping: Refboolean用于叠加嵌套深度计数和把手势状态暴露给 Overlay 渲染data-swiping。nestedSwipeProgressStore是一个极简的发布/订阅对象utils.ts只保存一个progress数字subscribe返回取消订阅函数。不使用 Vue 响应式避免每次滑动进度变化都触发组件重渲染——这是整个组件 60fps 性能设计的关键点之一。Props API 详解设计文档完整给出了各部件 Props 的语义以下结合源码逐一展开。DrawerRootPropTypeDefaultDescriptionopenboolean—受控 open 状态v-model:opendefaultOpenbooleanfalse初始 open 状态modalboolean \| trap-focustrue启用焦点陷阱 滚动锁定trap-focus仅陷阱焦点swipeDirectionSwipeDirectiondown滑动关闭的方向snapPointsDrawerSnapPoint[]—预设高度分数、px、remsnapPointDrawerSnapPoint \| null—受控的当前吸附点v-model:snapPointdefaultSnapPointDrawerSnapPoint \| null—初始吸附点snapToSequentialPointsbooleanfalse基于速度 vs 基于距离的吸附选择Emitsupdate:open携带details.reason、update:openComplete、update:snapPoint。需要特别指出源码与设计文档的一处差异设计文档中snapToSequentialPoints默认值为true而当前源码 DrawerRoot.vue 中默认值为false这一改动记录在后续的 BaseUI 对齐修复计划 中目的正是与 Base UI 的行为保持一致——默认按最近距离吸附。同时modal的类型也被拓宽为三档true默认完整模态焦点陷阱 隐藏其他内容useHideOthers 阻止外部指针事件 滚动锁定trap-focus陷阱焦点但不阻止外部指针交互适合非模态侧边面板false完全非模态。这一分层实现见 DrawerContent.vueisFullModal、isTrapFocusOnly、shouldTrapFocus三个计算属性决定FocusScope的trapped值以及DismissableLayer的disable-outside-pointer-events值。DrawerRoot还通过默认插槽向使用者暴露{ open, close }因此可以在任意位置通过v-slot{ close }编程式关闭抽屉。DrawerContentPropTypeDefaultDescriptionforceMountbooleanfalse保持挂载以便自行控制动画继承全部DismissableLayerprops与DialogContent相同包括as/asChild、onEscapeKeyDown、onPointerDownOutside等。EmitsopenAutoFocus、closeAutoFocus、escapeKeyDown、pointerDownOutside、focusOutside、interactOutside。源码在DismissableLayerProps基础上增加了initialFocus与finalFocus两个 props见 DrawerContentImpl.vueinitialFocustrue默认聚焦内部第一个可聚焦元素/false不聚焦/ HTMLElement 引用聚焦指定元素finalFocustrue默认聚焦回 Trigger/false不恢复焦点/ HTMLElement 引用。这两个 props 直接接入FocusScope的mount-auto-focus/unmount-auto-focus事件流。DrawerOverlayPropTypeDefaultDescriptionforceMountbooleanfalse保持挂载forceRenderbooleanfalse嵌套时也渲染源码 DrawerOverlay.vue 显示Overlay只在modal为真时渲染且默认在嵌套抽屉中跳过渲染除非设置forceRender。渲染内容被Presence包裹以支持淡入淡出动画。DrawerHandlePropTypeDefaultDescriptionasstring \| Componentdiv渲染的元素asChildbooleanfalse将 props 合并到子元素从DrawerContentImpl上下文绑定滑动手势事件。没有额外 props——样式完全交给使用方通过 CSS 控制。它本质上是纯装饰性的aria-hidden整个DrawerContent无论是否从把手出发都是可拖拽的。DrawerSwipeAreaPropTypeDefaultDescriptionswipeDirectionSwipeDirectionRoot 的反方向覆盖滑动方向disabledbooleanfalse禁用滑入打开默认监听 RootswipeDirection的反方向——例如默认底部抽屉SwipeArea监听顶部边缘从屏幕上方边缘下拉即可打开抽屉。DrawerProvider/DrawerIndent/DrawerIndentBackgroundDrawerProvider无 props不渲染任何 HTML 元素是纯逻辑协调器。DrawerIndent/DrawerIndentBackground标准的as/asChildprops无其他 props。Snap Points吸附点的解析与释放数学类型定义type DrawerSnapPoint number | stringutils.ts。解析规则见 useDrawerSnapPoints.ts 的parseSnapPoint0 ≤ n ≤ 1→ 视口高度的分数如0.5 50vhn 1→ 原始像素值148px字符串 → 解析为像素30rem字符串 →rem × document.documentElement.fontSize未知单位如%、vh、非有限值或负值如abcpx、-10px→ 返回null并被过滤丢弃避免 NaN 污染几何计算。useDrawerSnapPoints组合式函数承担吸附点的全部计算输入snapPoints、activeSnapPoint、popupHeight、视口元素 ref用 VueUse 的useResizeObserver观察弹层高度变化并在窗口resize时更新window.innerHeight作为视口高度在1px 容差内去重Math.abs(r.height - height) 1视为同一个点返回resolvedSnapPoints[]每项{ value, height, offset }其中offset max(0, popupHeight - height)即从完全打开位置到该点需要平移的距离以及activeSnapPointOffsetactiveSnapPoint的匹配同时支持按原始值或按解析高度等价匹配——允许受控用法在16rem与256、0.5与400之间自由切换而不破坏释放计算。释放时的吸附决策snapToNearest是这套系统的核心算法常量与公式从 Base UI 的DrawerViewport.tsx移植见 useDrawerSnapPoints.tsconst SNAP_VELOCITY_THRESHOLD 0.5 // px/ms 速度阈值超过才施加速度助推 const SNAP_VELOCITY_MULTIPLIER 300 // 速度对目标位移的放大系数 const MAX_SNAP_VELOCITY 4 // 速度钳制上限 const FAST_SWIPE_VELOCITY 0.5 // 顺序吸附模式下的快速甩动阈值流程分四步计算拖拽目标位移dragTargetOffset clamp(currentOffset dragDelta, 0, popupHeight)其中dragDelta以关闭方向为正getDisplacement按方向翻转符号。速度助推仅当速度绝对值超过阈值时targetOffset dragTargetOffset clampedVel * SNAP_VELOCITY_MULTIPLIER——快速甩动会越过当前点直接落到更远的目标。最近点选择遍历所有吸附点取与targetOffset距离最近者。关闭 vs 吸附只有当targetOffset距离完全关闭offset popupHeight比距离任何吸附点都更近时才关闭否则吸附到最近点。在snapToSequentialPoints true的顺序模式下行为改为每次最多前进一步仅当速度方向与拖拽方向一致且速度足够快或者目标位移物理跨越了相邻吸附点时才移动到相邻点。这也验证了设计文档中的描述snapToSequentialPointstrue时用速度决定移动一步否则按距离找最近点。手势系统useSwipeDismiss的事件策略useSwipeDismiss是整个抽屉的手感来源位于 composables/useSwipeDismiss.ts。其输入接口与设计文档一致interface UseSwipeDismissOptions { enabled: MaybeRefboolean elementRef: RefHTMLElement | null directions: SwipeDirection[] movementCssVars: { x: string, y: string } swipeThreshold?: number | ((opts: { element: HTMLElement, direction: SwipeDirection }) number) ignoreScrollableAncestors?: boolean canStart?: () boolean onDismiss?: () void onProgress?: (progress: number, details?: SwipeProgressDetails) void onCancel?: () void onSwipeStart?: () void onRelease?: (velocity: { x: number, y: number }) void onSwipingChange?: (swiping: boolean) void }事件策略Pointer 与 Touch 双通道Pointer 事件pointerdown/pointermove/pointerup通过 VueUse 的useEventListener绑定覆盖鼠标与手写笔。使用setPointerCapture确保指针移出元素边界后仍能追踪运动。源码还做了一个健壮性兜底把pointerup/pointercancel镜像到document把lostpointercapture与窗口blur都视为手势中断并调用finishSwipe——避免释放发生在窗口外、其他应用上时手势永久卡死在data-swiping状态。Touch 事件touchstart/touchmove/touchend面向移动端touchmove以passive: false绑定一旦确认手势即调用preventDefault()阻断原生滚动。滚动冲突检测touchstart时向上遍历 DOMfindScrollableAncestor寻找可滚动祖先首次移动时用canSwipeFromScrollEdge判断——只有滚动容器处于相关边缘如底部抽屉要求scrollTop 0、顶部抽屉要求scrollTop clientHeight scrollHeight时才允许把拖拽判给抽屉否则让给原生滚动。这一滚动边缘逻辑替代了原先遇到滚动容器就整体取消的粗暴方案与 Base UI 的canSwipeFromScrollEdgeOnPendingMove对齐。轴锁定与交叉轴仲裁触摸场景下还实现了轴锁定与交叉轴仲裁shouldYieldTouchMove若抽屉沿垂直轴关闭、而页面里还有水平滚动容器则从起点开始测量位移用AXIS_LOCK_SLOP 6与AXIS_LOCK_BIAS 2两个常量决定手势归属——交叉轴先超过阈值则整个手势交给原生滚动preserveNativeCrossAxisScroll true抽屉轴先超过阈值则锁定给抽屉并持续preventDefault()。速度追踪每次 move 记录{ x, y, timestamp }采样点释放时计算 px/ms 速度相邻采样间隔下限 16ms采样点超过 80ms 视作无速度。速度被用于两个决策快速甩动flick时移动到相邻吸附点无吸附点场景下速度超过0.3也视为有效关闭意图。速度数据还会交给onRelease由DrawerContentImpl计算释放强度标量。方向阻尼橡皮筋手感当用户朝错误方向拖拽例如底部抽屉向上拉过头即完全打开位置之上的过冲会施加平方根阻尼dampedOffset sqrt(overshoot)源码applyDirectionalDamping实现useSwipeDismiss.ts。允许的方向线性通过不允许的方向按x ** 0.5弹性压缩——这正是把抽屉往上多拽一点会感到越来越重的物理反馈设计文档注明与 Base UI 手感完全一致。反转取消拖拽超过默认阈值DEFAULT_SWIPE_THRESHOLD 40px的一半后再往回拖超过REVERSE_CANCEL_THRESHOLD 10px即判定为取消手势cancelledSwipe true释放时不关闭抽屉而是回弹复位。返回值{ swipePointerProps: { onPointerDown, onPointerMove, onPointerUp } swipeTouchProps: { onTouchStart, onTouchMove, onTouchEnd } isSwiping: Refboolean swipeDirection: RefSwipeDirection | undefined dragOffset: Ref{ x: number; y: number } }实际源码将其中的事件处理器直接绑定在元素上通过 watchelementRef动态挂接/卸载监听器对外返回isSwiping、swipeDirection、dragOffset。CSS 变量60fps 的秘诀所有滑动相关的 CSS 变量都通过element.style.setProperty命令式写入绕过 Vue 响应式从而在拖拽高频更新下保持 60fps设计文档明确说明这一设计动机。变量清单CSS Var设置者用途--drawer-swipe-movement-xDrawerContentImpl实时拖拽 X 偏移--drawer-swipe-movement-yDrawerContentImpl实时拖拽 Y 偏移--drawer-snap-point-offsetDrawerContentImpl吸附点位置平移--drawer-heightDrawerContentImpl弹层高度ResizeObserver 测量--drawer-frontmost-heightDrawerContentImpl最顶层打开抽屉的高度--drawer-swipe-progressDrawerOverlayDrawerIndent遮罩/缩放的滑动进度0–1--drawer-swipe-strengthDrawerOverlay阻力系数--nested-drawersDrawerContentImpl嵌套深度计数源码中变量名集中在 utils.ts 的DRAWER_CSS_VARS。关键实现细节CSS.registerPropertyregisterDrawerCssProperties()对长度类变量--drawer-swipe-movement-x/y、--drawer-snap-point-offset以{ syntax: length, inherits: false, initialValue: 0px }注册对数值类变量--drawer-swipe-progress、--drawer-swipe-strength以{ syntax: number, inherits: false }注册且带全局去重标志只注册一次。inherits: false避免了样式重算的级联开销。吸附点偏移写入顺序DrawerContentImpl在onRelease中先同步写新吸附点偏移、再清空移动偏移DrawerContentImpl.vue保证同一个动画帧内 transform 从手指位置连续过渡到新吸附点避免先回弹再动画的视觉跳帧。--drawer-swipe-strength释放时由computeSwipeReleaseScalar计算 0.1–1.0 的标量utils.ts输入为剩余距离与释放速度距离短、速度快 → 标量趋近 0.1快速释放距离长、速度慢 → 趋近 1。使用方可用它缩放 CSS 过渡时长让释放动画速度匹配甩动速度。嵌套高度保持useResizeObserver回调在有嵌套抽屉打开且已有测量高度时跳过写入keepHeightWhileNested保证子抽屉动画期间父抽屉的吸附几何保持稳定。Data Attributes样式与动画的挂载点应用到DrawerContent上的数据属性AttributeValueMeaningdata-open抽屉已打开data-closed抽屉已关闭data-swiping正在执行滑动手势data-swipe-directionup\|down\|left\|right当前滑动方向data-starting-style进入动画期间data-ending-style退出动画期间data-expanded处于全高吸附点data-nested-drawer-open子抽屉已打开源码中data-state、data-swipe-direction、data-swiping、data-nested-drawer-open由 DrawerContentImpl.vue 的dataAttributes计算属性 实际输出。用户文档 drawer.md 中展示了它们与动画 CSS 的组合用法例如.DrawerContent { /* 拖拽时跟随指针 */ transform: translateY(var(--drawer-swipe-movement-y, 0px)); transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1); } /* 进入/退出动画使用独立的 translate 属性与拖拽 transform 叠加而不互相覆盖 */ .DrawerContent[data-stateopen] { animation: slideIn 450ms cubic-bezier(0.32, 0.72, 0, 1); } .DrawerContent[data-stateclosed] { animation: slideOut 450ms cubic-bezier(0.32, 0.72, 0, 1); } /* 拖拽进行中把过渡时长归零让抽屉紧贴指针 */ .DrawerContent[data-swiping] { transition-duration: 0ms; } keyframes slideIn { from { translate: 0 100%; } } keyframes slideOut { to { translate: 0 100%; } }用户文档还给出了一个很实用的出血bleed技巧为抽屉锚定边缘增加padding 负margin让反方向拖拽时显示的是拉伸效果而非抽屉脱离边缘露出遮罩的穿帮.DrawerContent { --bleed: 48px; padding-bottom: calc(env(safe-area-inset-bottom, 0px) var(--bleed)); margin-bottom: calc(-1 * var(--bleed)); } keyframes slideIn { from { translate: 0 calc(100% - var(--bleed)); } } keyframes slideOut { to { translate: 0 calc(100% - var(--bleed)); } }Provider / Indent 效果iOS 卡片堆叠DrawerProvider全局协调器无 props、无 DOM 输出。职责源码见 DrawerProvider.vue用Mapstring, booleancontentId → open追踪所有抽屉的开关状态拥有visualStateStore纯发布/订阅对象{ swipeProgress, frontmostHeight }无 Vue 响应式滑动期间零重渲染开销通过 provide 暴露{ active, setDrawerOpen, removeDrawer, visualStateStore }active是任意抽屉处于打开状态的计算属性。DrawerIndentBackground读取 provider 的active状态仅渲染data-active/data-inactive属性不订阅 store——纯 CSS 样式目标通常是缩进页面后露出的纯色背景层。DrawerIndent包裹页面内容的主体包装器在onMounted订阅visualStateStoreonUnmounted退订把--drawer-swipe-progress和--drawer-height写到自身 DOM 节点上同时渲染data-active/data-inactive。CSS 侧即可根据这两个变量实现页面随滑动进度缩放/缩进的卡片堆叠效果。嵌套抽屉父子协同嵌套抽屉如 A 抽屉里再开 B 抽屉的实现要点DrawerRoot可选注入父级DrawerRootContext非嵌套时返回undefined嵌套时注册父级回调notifyParentFrontmostHeight等使父弹层能收到data-nested-drawer-open属性与正确的高度 CSS 变量--nested-drawersCSS 变量统计嵌套深度nestedOpenDrawerCount递增/递减供 CSS 侧做缩放/偏移变换DrawerOverlay在嵌套场景下默认跳过渲染除非forceRender父弹层通过订阅nestedSwipeProgressStore把子抽屉的滑动进度写到自己的--drawer-swipe-progress上——子抽屉滑走时父页面反向做视觉反馈。子抽屉的滑动进度通过onNestedSwipeProgressChange同时写入nestedSwipeProgressStore并转发给父级实现父子间无重渲染的进度同步DrawerRoot.vue。无障碍面向 WAI-ARIA Dialog 模式Drawer 遵循 Dialog WAI-ARIA 设计模式见 drawer.md 的声明设计文档列出的无障碍契约DrawerContent上设置roledialogaria-labelledby→DrawerTitle的 idaria-describedby→DrawerDescription的 idmodaltrue时焦点被困在抽屉内FocusScope的looptrapped关闭后焦点返回触发器开发环境process.env.NODE_ENV ! production下缺少DrawerTitle时输出 console 警告。键盘交互见 drawer.mdSpace/Enter开关抽屉Tab/ShiftTab在可聚焦元素间移动Esc关闭并聚焦回DrawerTrigger。测试验证Drawer 模块配有完整的测试保障位于 packages/core/src/DrawerDrawer.test.ts无障碍、键盘、点击关闭等行为测试Drawer.snap.test.ts快照测试composables/useSwipeDismiss.test.ts手势组合式函数的单元测试composables/useDrawerSnapPoints.test.ts吸附点解析与释放数学的单元测试utils.test.ts工具函数如位移换算、释放标量计算测试。后续的 BaseUI 对齐修复计划 中还规划了专门的Drawer.swipe.test.ts通过simulateSwipe(el, { from, to, duration })辅助函数stubperformance.now派发 pointer 事件序列来验证四个方向的阈值关闭、反转取消、最近点吸附、越过关闭阈值关闭以及顺序模式快速滑动等场景。运行方式pnpm --filter reka-ui/core test Drawer或项目的 typecheck 命令。完整使用示例安装reka-ui后按 drawer.md 的 Anatomy 组装各部件script setup import { DrawerClose, DrawerContent, DrawerDescription, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTitle, DrawerTrigger, } from reka-ui /script template DrawerRoot DrawerTrigger / DrawerPortal DrawerOverlay / DrawerContent DrawerHandle / DrawerTitle / DrawerDescription / DrawerClose / /DrawerContent /DrawerPortal /DrawerRoot /template吸附点用法snapPoints支持分数、像素与 rem 字符串配合v-model:snap-point读写当前吸附点script setup import { ref } from vue import { DrawerContent, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTrigger } from reka-ui const snapPoints [0.4, 0.75, 1] const activeSnapPoint ref(0.4) /script template DrawerRoot v-model:snap-pointactiveSnapPoint :snap-pointssnapPoints DrawerTriggerOpen/DrawerTrigger DrawerPortal DrawerOverlay / DrawerContent.../DrawerContent /DrawerPortal /DrawerRoot /template侧边抽屉通过swipe-directionright切换锚定边缘CSS 侧改用 X 轴变量与关键帧非模态通过modalfalse或modaltrap-focus区分关闭原因通过update:open事件里的details?.reason如swipe、escape-key。此外DrawerContent还可通过DrawerPopup别名引用、DrawerOverlay通过DrawerBackdrop别名引用、滚动容器通过可选的DrawerViewport提供三者均与 Base UI 的命名对齐见 index.ts。总结这份设计文档描述了一个为手感而生的抽屉组件它不是 Dialog 的简单包装而是围绕useSwipeDismiss手势引擎与useDrawerSnapPoints吸附算法全新构建的独立体系。其最具借鉴价值的设计决策有三点一是全部高频状态走命令式 CSS 变量 非响应式发布/订阅 store以 60fps 为目标的性能纪律贯穿始终二是滚动冲突与交叉轴仲裁的精细处理让抽屉里还能滚动成为可能三是Base UI 数学公式的逐行移植阻尼、释放标量、吸附速度助推保证了跨框架的交互一致性。对于想要理解现代前端手势密集型组件如何平衡体验、性能与可维护性的开发者这份设计与它的源码实现是一份难得的完整教材。【免费下载链接】radix-vueAn open-source UI component library for building high-quality, accessible design systems and web apps for Vue. Previously Radix Vue项目地址: https://gitcode.com/GitHub_Trending/ra/radix-vue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考