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 to
Indent.—— 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
| Name | Description | Type | Required | Default |
|---|---|---|---|---|
as | The element or component this component should render as. Can be overwritten byasChild. | AsTag \| Component | No | "div" |
asChild | Change the default rendered element for the one passed as a child, merging their props and behavior. See the Composition guide for more details. | boolean | No | - |
Data Attributes(来自 Drawer.md 的 DataAttributesTable)
| Attribute | Values |
|---|---|
[data-active] | Present when a drawer is open |
[data-inactive] | Present when no drawer is open |
两个属性都继承自PrimitiveProps(as/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>从源码结构看,可以提炼出四条实现事实:
- 默认渲染为
div。withDefaults(..., { as: 'div' })与 API 表中"div"的默认值严格对应;通过as/asChild可替换为任意元素,这是全库统一的Primitive组合模式。 - 状态来自
DrawerProvider上下文而非自身。injectDrawerProviderContext(null)表明该组件必须放在DrawerProvider提供的上下文内使用,active是一个响应式信号,表示“当前是否有任何抽屉处于打开状态”。 data-active/data-inactive二选一地挂载。打开时挂data-active(空字符串表示存在),关闭时挂data-inactive。注意这里的判断是!providerContext?.active.value——上下文缺失时也视为 inactive。这两个数据属性正是 CSS 侧唯一的“开关”,全部样式决策都交给开发者自己的 CSS。- 自身不做任何变换、定位或动画。组件没有操作 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 中createNestedSwipeProgressStore的getSnapshot / subscribe模式一致); DrawerIndent在挂载时subscribe(sync),每次拖动帧都会把最新快照写到自身元素的内联样式上:--drawer-swipe-progress(0表示静止、越接近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 个变量;其中swipeProgress与swipeStrength通过CSS.registerProperty注册为<number>类型(swipeProgress初始值0、swipeStrength初始值1),这正是浏览器能够对这两个变量做平滑transition/animation插值的前提。
实战用法:成对使用并编写缩进动画
Indent与IndentBackground均从reka-ui包导出(见 index.ts:DrawerIndent、DrawerIndentBackground及其 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),仅供参考