Sticky
Headless sticky/affix wrapper that pins with native position: sticky and reports stuck state.
|
|
| Package |
@vizejs/ui/sticky |
| Maturity |
stable |
| Own the source |
vize lib pull sticky |
| Requires |
measure |
| Aliases |
sticky, affix, sticky header, pinned |
| Covers |
CSS position: sticky, Ant Design Affix, IntersectionObserver stuck detection |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { Sticky, Affix } from "@vizejs/ui/sticky";
Or copy the source into your project with vize lib pull sticky (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Section navigation that pins to the top of a scrollable article and reports when it is stuck. -->
<script setup lang="ts">
import { useId, useTemplateRef } from "vue";
import { Sticky } from "@vizejs/ui/sticky";
const titleId = useId();
const article = useTemplateRef<HTMLElement>("article");
// The article scrolls on its own, so it needs a fixed block size.
const scrollerSize = { blockSize: "14rem", overflow: "auto" };
const chapters = ["Overview", "Setup", "Usage", "Recipes", "FAQ"];
</script>
<template>
<section ref="article" :aria-labelledby="titleId" :style="scrollerSize">
<h2 :id="titleId">Handbook</h2>
<Sticky v-slot="{ stuck }" as="nav" :root="article" :offset="0" aria-label="Chapters">
<a href="/handbook/overview">Overview</a>
<a href="/handbook/setup">Setup</a>
<a href="/handbook/usage">Usage</a>
<span v-if="stuck">(pinned)</span>
</Sticky>
<p v-for="chapter in chapters" :key="chapter">
{{ chapter }}: this chapter is long enough to scroll the navigation out of its place.
</p>
</section>
</template>
API
Sticky
Source: src/families/layout/sticky/sticky.vue
Props
| Prop |
Type |
Default |
Description |
as |
keyof HTMLElementTagNameMap |
"div" |
Native element rendered as the sticky box, for example header or nav. |
side |
StickySide |
"top" |
Edge the element pins to. |
offset |
number |
0 |
Distance in CSS pixels between the pinned edge and the scroll container edge. |
root |
HTMLElement | null |
null |
Scroll container used to detect the stuck state. null uses the viewport. |
disabled |
boolean |
false |
Render in normal flow without position: sticky or stuck detection. |
Events
| Event |
Payload |
Description |
stuck-change |
[stuck: boolean] |
Fired when the element becomes pinned or releases. |
Slots
| Slot |
Slot props |
Description |
default |
StickySlotState |
Sticky contents. Receives the stuck state. |
Exposed
| Member |
Type |
Description |
stuck |
boolean |
Whether the element is currently pinned at its offset. |
state |
StickyState |
Stable state token for styling and tests. |
side |
StickySide |
Pinned edge. |
element |
HTMLElement | null |
Rendered sticky element or component instance root. |
refresh |
() => boolean |
Re-read geometry and update stuck immediately. Returns the new value. |
Affix
Source: src/families/layout/sticky/sticky.vue
Props
| Prop |
Type |
Default |
Description |
as |
keyof HTMLElementTagNameMap |
"div" |
Native element rendered as the sticky box, for example header or nav. |
side |
StickySide |
"top" |
Edge the element pins to. |
offset |
number |
0 |
Distance in CSS pixels between the pinned edge and the scroll container edge. |
root |
HTMLElement | null |
null |
Scroll container used to detect the stuck state. null uses the viewport. |
disabled |
boolean |
false |
Render in normal flow without position: sticky or stuck detection. |
Events
| Event |
Payload |
Description |
stuck-change |
[stuck: boolean] |
Fired when the element becomes pinned or releases. |
Slots
| Slot |
Slot props |
Description |
default |
StickySlotState |
Sticky contents. Receives the stuck state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
refresh |
— |
|
side |
— |
|
state |
— |
|
stuck |
— |
|
Behavior
Normative behavior for @vizejs/ui/sticky (Sticky, alias Affix). sticky.vue pins with
native position: sticky — no scroll listeners move the element — and only observes geometry
to report whether it is currently pinned. Every row is proven by the named test.
| State x input |
Observable outcome |
Proven by |
| any render |
The native element (as) carries position: sticky, the side inset, --vize-sticky-offset, data-side, and data-state. |
renders native sticky positioning with offset variables and slot state |
| mounted |
An IntersectionObserver watches the element with a root margin that trims offset + 1 px from the pinned edge. |
renders native sticky positioning with offset variables and slot state |
| box rests on its offset line |
data-stuck="true", data-state="stuck", and stuck-change fires once with true. |
reports stuck when the box rests on its offset line and releases after |
| box sits below the line or scrolled away with its block |
data-stuck is removed and stuck-change fires once with false. |
reports stuck when the box rests on its offset line and releases after |
side="bottom" with a custom root |
Geometry is measured against the container bottom and the observer uses that root. |
bottom stickiness measures against the container bottom edge |
disabled |
Sticky positioning is removed and the observer disconnects; re-enabling reconnects with the current offset. |
disabled stickiness drops positioning and observation |
expose refresh() |
Re-reads geometry synchronously and returns the new stuck value. |
expose refresh re-reads geometry on demand |
| geometry helpers |
isStickyStuck and stickyRootMargin describe the one-pixel pinned band on both edges. |
geometry helpers describe the pinned band on both edges |
| SSR |
Server markup is byte-identical, includes the sticky style, and never reports stuck. |
renders byte-identical sticky markup without stuck state on the server |
| hydration |
The server element is reused with zero warnings; observation starts after mount. |
hydrates the server box without diagnostics |
| public types |
Side, state, native tag, and numeric offset are closed contracts. |
src/families/layout/sticky/sticky.types.test-d.ts |
| DOM/SSR/Vapor |
sticky.vue compiles in every renderer lane. |
scripts/check-renderers.ts |
Parts And Data
| Target |
Public contract |
| Root |
part="root", data-vize-ui="sticky", data-side, data-state, data-stuck, data-disabled, --vize-sticky-offset |
Sticky ships no stylesheet beyond the inline sticky declaration required for the behavior.