news 2026/9/17 19:59:23

Reka UI Drawer 的 Indent 与 IndentBackground 组件:实现 iOS 式卡片堆叠背景效果的 API 与源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Reka UI Drawer 的 Indent 与 IndentBackground 组件:实现 iOS 式卡片堆叠背景效果的 API 与源码解析

Reka UI Drawer 的 Indent 与 IndentBackground 组件:实现 iOS 式卡片堆叠背景效果的 API 与源码解析

【免费下载链接】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

本文聚焦 Reka UI(radix-vue,前身 Radix Vue)中DrawerIndentBackground组件的 API 定义、源码实现与配套用法。DrawerIndentBackground是 Drawer 组件族中专门用于“缩进(Indent)”视觉效果的背景层:当抽屉滑出时,页面内容整体缩放内缩,而它作为垫在缩放内容背后的一层背景从中“露”出来,从而复刻原生 iOS 的卡片堆叠(card stack)观感。读完本文,你可以掌握它的 Props API、data-active/data-inactive数据属性,理解它与DrawerIndent如何通过 Provider 上下文与 CSS 变量协作完成缩进动画,并可直接复制一段可用的 CSS 方案落地该效果。

DrawerIndentBackground 是什么:定位与适用场景

在 Drawer 组件文档 的 API Reference 中,该组件的定位只有一句话,但信息量足够大:

The backdrop layer revealed behind an indented page (typically a solid colour that peeks out as the page scales down). Companion toIndent.

—— Drawer.md 的 "IndentBackground" 小节

拆开来看有三层含义:

  • 它是一个背景层(backdrop layer),不是遮罩(modal overlay)。与DrawerOverlay(仅modal模式渲染的遮罩层)不同,DrawerIndentBackground服务的是非模态缩进场景:抽屉打开时页面保持可交互,只是视觉上被“压小”;
  • 它存在的意义是“被露出来”。页面内容被DrawerIndent包裹并随抽屉滑出而缩小(scale down)时,四周会空出一圈区域,这一层背景色正好填补空隙,形成“内容卡片浮在桌面背景上”的层次感;
  • 它是Indent配套件(Companion),两个组件必须成对理解:Indent负责“把内容缩进去”,IndentBackground负责“垫在后面兜底”。

整个 Drawer 组件族目前处于 Alpha 阶段(见 Drawer.md 顶部的 Alpha 徽标),其设计背景与 Base UI 的对齐目标可在仓库内的设计文档 2026-04-04-drawer-design.md 中找到补充。

API 参考:完整继承 PropsTable 的定义

组件 API 文档 DrawerIndentBackground.md(自动生成文件)给出的 Props 如下表,该表即组件的完整属性面:

Props

NameDescriptionTypeRequiredDefault
asThe element or component this component should render as. Can be overwritten byasChild.AsTag \| ComponentNo"div"
asChildChange the default rendered element for the one passed as a child, merging their props and behavior. See the Composition guide for more details.booleanNo-

Data Attributes(来自 Drawer.md 的 DataAttributesTable)

AttributeValues
[data-active]Present when a drawer is open
[data-inactive]Present when no drawer is open

两个属性都继承自PrimitivePropsas/asChild@/Primitive提供),组件自身没有额外引入业务属性——这是刻意为之:动画状态不通过 Props 传递,而是走 Provider 上下文 + CSS 变量通道(见下文源码分析)。

源码解析:一个刻意保持极简的“状态标记”元素

实现文件 DrawerIndentBackground.vue 全文不到 30 行,核心逻辑只有三处:

<script setup lang="ts"> import { Primitive } from '@/Primitive' import { useForwardExpose } from '@/shared' import { injectDrawerProviderContext } from './DrawerProvider.vue' const props = withDefaults(defineProps<DrawerIndentBackgroundProps>(), { as: 'div' }) const { forwardRef } = useForwardExpose() const providerContext = injectDrawerProviderContext(null) </script> <template> <Primitive v-bind="props" :ref="forwardRef" :data-active="providerContext?.active.value ? '' : undefined" :data-inactive="!providerContext?.active.value ? '' : undefined" > <slot /> </Primitive> </template>

从源码结构看,可以提炼出四条实现事实:

  1. 默认渲染为divwithDefaults(..., { as: 'div' })与 API 表中"div"的默认值严格对应;通过as/asChild可替换为任意元素,这是全库统一的Primitive组合模式。
  2. 状态来自DrawerProvider上下文而非自身injectDrawerProviderContext(null)表明该组件必须放在DrawerProvider提供的上下文内使用,active是一个响应式信号,表示“当前是否有任何抽屉处于打开状态”。
  3. data-active/data-inactive二选一地挂载。打开时挂data-active(空字符串表示存在),关闭时挂data-inactive。注意这里的判断是!providerContext?.active.value——上下文缺失时也视为 inactive。这两个数据属性正是 CSS 侧唯一的“开关”,全部样式决策都交给开发者自己的 CSS。
  4. 自身不做任何变换、定位或动画。组件没有操作 CSS 变量、没有监听手势,这是一个纯展示层。这一点与它的姊妹组件形成鲜明对比,也是理解二者分工的关键。

与 DrawerIndent 的分工:状态订阅与 CSS 变量同步在 Indent 一侧

DrawerIndent.vue 与DrawerIndentBackground结构几乎相同(同样as: 'div'、同样注入 Provider 上下文、同样输出data-active/data-inactive),但它额外承担了一个IndentBackground没有的职责——订阅视觉状态 Store 并同步 CSS 变量

// DrawerIndent.vue(节选) onMounted(() => { const store = providerContext?.visualStateStore if (!store) return const el = currentElement.value if (!el) return const sync = () => { const { swipeProgress, frontmostHeight } = store.getSnapshot() el.style.setProperty(DRAWER_CSS_VARS.swipeProgress, swipeProgress > 0 ? `${swipeProgress}` : '0') if (frontmostHeight > 0) el.style.setProperty(DRAWER_CSS_VARS.height, `${frontmostHeight}px`) else el.style.removeProperty(DRAWER_CSS_VARS.height) } sync() unsubscribe = store.subscribe(sync) })

对照 Drawer.md 对 Indent 的描述("Reads the visual state from a parentDrawerProviderand syncs the--drawer-swipe-progressand--drawer-heightCSS variables onto its element"),源码印证了完整的状态链路:

  • 用户拖动抽屉时,DrawerContent在拖动过程中把位移、进度写入 Provider 的visualStateStore(一个快照 + 订阅式的 Store,与 utils.ts 中createNestedSwipeProgressStoregetSnapshot / subscribe模式一致);
  • DrawerIndent在挂载时subscribe(sync),每次拖动帧都会把最新快照写到自身元素的内联样式上:--drawer-swipe-progress0表示静止、越接近1表示越接近完全划走)和--drawer-height(最前层抽屉的高度,frontmostHeight > 0时设置,否则移除);
  • 卸载时还会主动清理:swipeProgress复位为0、移除height变量,避免残留内联样式污染后续打开的抽屉。

DrawerIndentBackground完全不碰这些变量——它只需要知道“开没开”(data-active/data-inactive),把“缩多少”的计算全部交给与Indent共享同一组 CSS 变量的 CSS 规则。这种“一个订阅数据、一个标记状态”的分工,让背景层可以零 JS 开销地参与动画。

顺带一提,变量名统一定义在 utils.ts 的DRAWER_CSS_VARS常量中,包括--drawer-swipe-progress--drawer-height--drawer-frontmost-height--drawer-swipe-strength等 8 个变量;其中swipeProgressswipeStrength通过CSS.registerProperty注册为<number>类型(swipeProgress初始值0swipeStrength初始值1),这正是浏览器能够对这两个变量做平滑transition/animation插值的前提。

实战用法:成对使用并编写缩进动画

IndentIndentBackground均从reka-ui包导出(见 index.ts:DrawerIndentDrawerIndentBackground及其 Props 类型),且自动注册组件中也有登记(components.ts)。典型的“抽屉 + 缩进页面”结构如下:

<script setup> import { DrawerContent, DrawerDescription, DrawerHandle, DrawerIndent, DrawerIndentBackground, DrawerPortal, DrawerRoot, DrawerTitle, DrawerTrigger, } from 'reka-ui' </script> <template> <div class="page"> <!-- 背景层:垫在缩进内容之后,抽屉打开时从四周露出 --> <DrawerIndentBackground class="page-background" /> <!-- 缩进层:包裹会随抽屉缩放的页面内容 --> <DrawerIndent class="page-indent"> <!-- 页面主体内容 --> <main>...</main> </DrawerIndent> <DrawerRoot :modal="false"> <DrawerTrigger class="drawer-button">Open</DrawerTrigger> <DrawerPortal> <DrawerContent class="drawer-content-bottom"> <DrawerHandle /> <DrawerTitle>Drawer Title</DrawerTitle> <DrawerDescription>This drawer slides up from the bottom.</DrawerDescription> </DrawerContent> </DrawerPortal> </DrawerRoot> </div> </template>

要点:缩进效果通常搭配非模态抽屉使用(DrawerRoot:modal="false",见 Drawer.md "Non-modal drawer" 小节),因为模态模式下焦点被锁、页面不可交互,卡片堆叠的视觉意义就减弱了。DrawerIndent/DrawerIndentBackground需要能注入到 Provider 上下文,因此应放置在提供 Drawer 上下文的层级之内。

动画完全由 CSS 驱动。参照 Drawer.md "Animating the drawer" 小节对--drawer-swipe-*变量族的说明,一套可用的缩进样式如下(变量取值依据均来自 utils.ts 与 DrawerIndent.vue):

/* 背景层:只靠 contenteditable="false">【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 19:58:39

用Python实现POD本征正交分解降维及GUI可视化工具

简介&#xff1a;一份面向数据科学、流体力学、结构健康监测等领域研究者的POD&#xff08;本征正交分解&#xff09;数据降维模型完整工程实例&#xff0c;以Python实现为主线&#xff0c;围绕数据预处理、协方差矩阵构建、特征值分解、能量截断、降维转换与数据重构等核心模块…

作者头像 李华
网站建设 2026/9/17 19:55:34

SQL Server 2022安装全攻略:从版本选择到避坑指南

1. 版本选择不纠结&#xff1a;从根本需求倒推该装哪一款先泼一盆冷水&#xff1a;如果你搜到的是“SQL Server 2008 R2下载”之类的老教程&#xff0c;我建议你直接关掉那个页面。这不是说老教程讲得不对&#xff0c;而是SQL Server的版本迭代跨度实在太大&#xff0c;2008 R2…

作者头像 李华
网站建设 2026/9/17 19:55:01

Linux 网络配置:netplan 与后端管理器分工排查

一台机器上装着 Ubuntu Server&#xff0c;你改完/etc/netplan/01-netcfg.yaml敲下netplan apply&#xff0c;ip a一看地址纹丝不动&#xff1b;换一台机器&#xff0c;同事用nmcli配好的静态路由重启后凭空消失&#xff1b;再换一台容器宿主机&#xff0c;/etc/systemd/networ…

作者头像 李华
网站建设 2026/9/17 19:54:40

Go语言实现单词笔记功能的技术实践

1. 项目背景与核心价值最近在给珊瑚单词这个背单词应用开发一个新功能——允许用户给单词添加个人笔记。作为一款主打高效记忆的单词工具&#xff0c;这个功能的加入将彻底改变用户的学习体验。想象一下&#xff0c;当你遇到一个总是记不住的单词时&#xff0c;能直接在应用里记…

作者头像 李华
网站建设 2026/9/17 19:53:51

Windows下用SQLCipher命令行解密加密数据库并导出明文SQLite

做数据库开发或者维护业务系统的&#xff0c;十有八九都碰到过这种场景&#xff1a;甲方丢过来一个.db文件&#xff0c;告诉你说这是系统里的业务数据库&#xff0c;但你用SQLite一打开&#xff0c;直接报错file is not a database。用记事本看一眼二进制内容&#xff0c;前面全…

作者头像 李华
网站建设 2026/9/17 19:52:42

Web渗透测试检查清单:信息收集、配置管理、认证授权与数据验证实战

简介&#xff1a;面向软件测试与安全评估人员&#xff0c;《渗透测试指导书.pdf》是一份可落地的操作型检查手册&#xff0c;用于规范Web系统渗透测试的流程与判定标准。文档以序号、检查项、检查内容、操作步骤、预期结果的结构展开&#xff0c;从WEB应用发现、端口扫描、错误…

作者头像 李华