顶部偏移
向下滚动检查清单。工具栏到达指定偏移后会停住,勾选进度始终可见。
发布前检查 · 0/6
随内容滚动吸附不会移动或重建工具栏里的控件。
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
Affix,
Card,
Checkbox,
Inline,
NumberInput,
ScrollArea,
Stack,
Switch,
Tag,
Text,
} from '@hina-ui/vue'
import { affixChecklist } from '../../affix'
const disabled = ref(false)
const offset = ref(12)
const checked = ref<boolean[]>(Array(6).fill(false))
const completed = computed(() => checked.value.filter(Boolean).length)
const items = affixChecklist('zh-CN')
</script>
<template>
<Stack class="w-full max-w-xl">
<Inline align="center" gap="lg" wrap>
<Switch v-model="disabled">禁用吸附</Switch>
<Inline align="center" gap="sm">
<Text size="sm" tone="muted">顶部偏移</Text>
<NumberInput
v-model="offset"
:min="0"
:max="40"
:step="4"
size="sm"
aria-label="顶部偏移(px)"
class="w-28"
/>
</Inline>
</Inline>
<Card :padded="false">
<ScrollArea class="h-80" :shadow="false" focusable label="发布前检查清单">
<Stack class="p-4" gap="lg">
<Text size="sm" tone="muted">
向下滚动检查清单。工具栏到达指定偏移后会停住,勾选进度始终可见。
</Text>
<Affix v-slot="{ affixed }" :offset="offset" :disabled="disabled">
<Card class="p-3" :class="affixed ? 'shadow-md' : 'shadow-none'">
<Inline align="center" justify="between" gap="sm" wrap>
<Text size="sm" weight="medium">发布前检查 · {{ completed }}/6</Text>
<Tag size="sm" :tone="affixed ? 'accent' : 'neutral'">
{{ affixed ? '已吸附' : '随内容滚动' }}
</Tag>
</Inline>
</Card>
</Affix>
<Checkbox
v-for="(item, index) in items"
:key="item.title"
v-model="checked[index]"
:description="item.description"
block
>
{{ item.title }}
</Checkbox>
<Text size="xs" tone="muted">吸附不会移动或重建工具栏里的控件。</Text>
</Stack>
</ScrollArea>
</Card>
</Stack>
</template>
用法
import { Affix } from '@hina-ui/vue'
将需要吸附的内容放进 Affix。默认贴到最近滚动区域的顶部,offset 设置与边缘的距离,单位 px。
<Affix :offset="16">
<Card>工具栏或操作区</Card>
</Affix>
Affix 使用原生 position: sticky,保留原来的布局空间、宽度和 DOM 节点。它不添加卡片、背景或滚动容器,也不为吸附加入位移、缩放或淡入淡出动画。外观由插槽内容决定。
示例
顶部偏移与禁用
顶部示例可以调整偏移、禁用吸附并勾选检查项。disabled 使内容恢复普通流,保留内部状态。用 #default="{ affixed }" 或 @change 获取当前是否贴到指定边缘;例如给已吸附的工具栏添加阴影。
<Affix v-slot="{ affixed }" :offset="16" :disabled="disabled">
<Card :class="affixed ? 'shadow-md' : 'shadow-none'">操作区</Card>
</Affix>
状态改变时尽量只调整颜色、边框颜色或阴影,避免改变高度和外边距,反复推动吸附阈值。
底部操作区
将底部操作区放在它自然应该出现的位置,设置 position="bottom"。当自然位置位于可视区域下方时,它留在底部;滚到原位置后与内容一起移动。底部吸附仍保留在当前父区域内。
编辑文章
在列表中展示的一段简短介绍。
为下一位编辑保留上下文。
修改仅保留在当前示例
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import {
Affix,
Button,
Card,
FormField,
Heading,
Inline,
Input,
ScrollArea,
Stack,
Text,
Textarea,
} from '@hina-ui/vue'
const draft = reactive({ title: '组件设计回顾', author: '设计团队', summary: '', notes: '' })
const saved = ref(false)
watch(draft, () => {
saved.value = false
})
</script>
<template>
<Card :padded="false" class="w-full max-w-xl">
<ScrollArea class="h-96" :shadow="false" focusable label="编辑文章">
<Stack class="p-4" gap="lg">
<Heading :level="3" size="base">编辑文章</Heading>
<FormField label="标题"><Input v-model="draft.title" /></FormField>
<FormField label="作者"><Input v-model="draft.author" /></FormField>
<FormField label="摘要" description="在列表中展示的一段简短介绍。">
<Textarea v-model="draft.summary" :rows="4" placeholder="这一篇主要讨论什么?" />
</FormField>
<FormField label="编辑备注" description="为下一位编辑保留上下文。">
<Textarea v-model="draft.notes" :rows="4" placeholder="补充修改原因或待确认事项" />
</FormField>
<Affix position="bottom" :offset="12">
<Card class="p-3">
<Inline align="center" justify="between" gap="sm" wrap>
<Text role="status" size="sm" tone="muted">
{{ saved ? '草稿已保存在当前示例' : '修改仅保留在当前示例' }}
</Text>
<Button size="sm" :disabled="!draft.title.trim() || saved" @click="saved = true">
保存草稿
</Button>
</Inline>
</Card>
</Affix>
</Stack>
</ScrollArea>
</Card>
</template>
父区域边界
每个 Affix 受父布局区域约束。下面每个标题只在自己的分组范围内停留,分组结束时自动离开,不会盖住后面的内容。
每组标题只留在自己的区域内。滚到下一组时,上一组标题会一起离开。
设计与内容
内容与文案
标题清楚,错误信息给出恢复方法,空状态提供下一步入口。
键盘操作
检查 Tab 顺序、焦点可见性,以及浮层关闭后的焦点恢复。
窄屏布局
用较窄的窗口检查长标题、工具栏换行和内容是否横向溢出。
运行与适配
加载与失败
慢速网络下保留现有内容,失败后能够重试,避免清空已经填写的字段。
主题与方向
检查深色模式的对比度,以及 RTL 下图标、间距和文本的排列。
首屏与水合
刷新页面,确认内容在脚本执行前可读,水合后没有意外跳动。
<script setup lang="ts">
import { Affix, Card, Heading, ScrollArea, Stack, Text } from '@hina-ui/vue'
import { affixChecklist } from '../../affix'
const items = affixChecklist('zh-CN')
const groups = [
{ title: '设计与内容', items: items.slice(0, 3) },
{ title: '运行与适配', items: items.slice(3) },
]
</script>
<template>
<Stack class="w-full max-w-xl" gap="sm">
<Text size="sm" tone="muted">
每组标题只留在自己的区域内。滚到下一组时,上一组标题会一起离开。
</Text>
<Card :padded="false">
<ScrollArea class="h-72" :shadow="false" focusable label="分组检查说明">
<Stack gap="none">
<Stack v-for="group in groups" :key="group.title" gap="none">
<Affix>
<Heading :level="3" size="sm" class="bg-surface border-line border-b px-4 py-3">
{{ group.title }}
</Heading>
</Affix>
<Stack v-for="item in group.items" :key="item.title" class="p-4" gap="sm">
<Text size="sm" weight="medium">{{ item.title }}</Text>
<Text size="sm" tone="muted">{{ item.description }}</Text>
</Stack>
</Stack>
</Stack>
</ScrollArea>
</Card>
</Stack>
</template>
滚动容器与布局
- 自动使用最近具有滚动机制的祖先;没有这类祖先时跟随页面视口。放进 ScrollArea 的内容区即可,不需要读取
viewport或传入target。 - 父区域需要留有移动空间。不要给 Affix 外面包一个仅与它等高的容器,否则没有可吸附的行程。横向 Flex 中需要侧栏保持自然高度时,给 Affix 设置
self-start,不要将它拉伸到整列高度。 overflow: auto / scroll / hidden会改变吸附参照;用于裁剪但不希望建立滚动区域时,可在适用场景使用overflow: clip。不要跨过一个不滚动的overflow: hidden祖先去期待页面级吸附。- 自动跟随容器宽度,保留原来的裁剪、方向和层叠关系。默认层级为
z-10,可通过class调整。需要一直固定在窗口角落的操作入口使用 FloatButton。
<ScrollArea class="h-96">
<Stack>
<Affix :offset="12"><Card>筛选与批量操作</Card></Affix>
<DataList :items="items" :get-key="item => item.id">
<template #default="{ item }">{{ item.title }}</template>
</DataList>
</Stack>
</ScrollArea>
SSR 与性能
吸附由 CSS 完成,SSR 首屏与客户端使用相同布局,没有测量后切成 fixed 的阶段。即使 JavaScript 尚未执行,内容仍可读,吸附仍生效。
affixed 插槽值在服务端为 false,挂载后同步当前边缘状态。滚动与尺寸变化只用于状态检测,同一帧合并处理,状态未改变时不触发事件;不会逐帧写入定位或占位尺寸。父区域结束、离开指定边缘时状态恢复为 false。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
as | string | 'div' | 根节点标签 |
position | 'top' | 'bottom' | 'top' | 吸附边缘 |
offset | number | 0 | 边缘偏移,单位 px;支持负值 |
disabled | boolean | false | 停用吸附,恢复普通流 |
class | string | — | 根节点样式;原生属性及 style 也传给根节点 |
Slots
| 插槽 | 参数 | 说明 |
|---|---|---|
default | { affixed: boolean } | 当前是否处在指定吸附边缘 |
Events
| 事件 | 参数 | 说明 |
|---|---|---|
change | affixed: boolean | 边缘吸附状态发生变化时触发 |
Expose
| 名称 | 类型 | 说明 |
|---|---|---|
element | HTMLElement | undefined | 根节点 |
affixed | boolean | 当前边缘吸附状态 |
update | () => void | 下一帧重新检测状态;普通滚动和尺寸变化自动处理 |
根节点提供 data-position、data-affixed 和 data-disabled,便于自定义样式。