Swipeable, scroll-snap pager with typed page ids and a segmented tab bar (APG tabs).
|
|
| Package |
@vizejs/ui/pager |
| Maturity |
stable |
| Own the source |
vize lib pull pager |
| Requires |
context, controllable-state, id |
| Aliases |
view pager, swipe tabs, segmented pager, page view, swipeable views |
| Covers |
WAI-ARIA Tabs, CSS scroll snap, Android ViewPager2, UIPageViewController |
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 { Pager, PagerTabList, PagerTab, PagerViewport, PagerPage } from "@vizejs/ui/pager";
Or copy the source into your project with vize lib pull pager (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
<!-- Controlled swipeable pager whose segmented tabs and scroll-snap pages stay in sync. -->
<script setup lang="ts">
import { ref } from "vue";
import { Pager, PagerPage, PagerTab, PagerTabList, PagerViewport } from "@vizejs/ui/pager";
type Library = "photos" | "albums" | "shared";
const pages: readonly Library[] = ["photos", "albums", "shared"];
const labels: Record<Library, string> = { photos: "Photos", albums: "Albums", shared: "Shared" };
const active = ref<Library>("photos");
// Swiping needs a horizontal scroll-snap viewport with one full-width page per snap point.
const viewportLayout = { display: "flex", overflowX: "auto", scrollSnapType: "x mandatory" };
const pageLayout = { flex: "0 0 100%", scrollSnapAlign: "start" };
</script>
<template>
<Pager v-model="active" :pages>
<PagerTabList aria-label="Library">
<PagerTab v-for="page in pages" :key="page" :page>{{ labels[page] }}</PagerTab>
</PagerTabList>
<PagerViewport :style="viewportLayout">
<PagerPage v-for="page in pages" :key="page" :page :style="pageLayout">
<p>{{ labels[page] }} in your library.</p>
</PagerPage>
</PagerViewport>
</Pager>
</template>
API
Source: src/families/navigation/pager/pager.vue
Props
| Prop |
Type |
Default |
Description |
pages (required) |
readonly PageId[] |
required |
Page ids in order; their literal union types v-model and change. |
modelValue |
PageId |
undefined |
Controlled active page. undefined selects uncontrolled behavior. |
defaultValue |
PageId |
pages[0] |
Initial uncontrolled page. |
id |
string | null |
undefined |
Base id for tabs and panels. null and undefined select a deterministic fallback. |
Events
| Event |
Payload |
Description |
update:modelValue |
[page: PageId] |
Fired when the active page requests a change. |
change |
[page: PageId, previous: PageId, reason: PagerChangeReason] |
Fired after the active page changes, with the previous page and what caused it. |
Slots
| Slot |
Slot props |
Description |
default |
PagerSlotState<PageId> |
Renders PagerTabList and PagerViewport with the typed page state. |
Exposed
| Member |
Type |
Description |
pages |
readonly PageId[] |
Page ids in order. |
active |
PageId |
Active page. |
index |
number |
Zero-based index of the active page. |
count |
number |
Number of pages. |
goTo |
(page: PageId) => boolean |
Scroll to a page and make it active. |
next |
() => boolean |
Go to the next page (no wrap); returns whether it moved. |
previous |
() => boolean |
Go to the previous page (no wrap); returns whether it moved. |
Source: src/families/navigation/pager/pager-tab-list.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name of the tab list. |
Slots
| Slot |
Slot props |
Description |
default |
Record<string, never> |
PagerTab segments, in page order. |
Source: src/families/navigation/pager/pager-tab.vue
Props
| Prop |
Type |
Default |
Description |
page (required) |
string |
required |
Page this segment selects; must be one of the Pager pages. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly selected: boolean } |
Segment label with its selection state. |
Source: src/families/navigation/pager/pager-viewport.vue
Props
| Prop |
Type |
Default |
Description |
settleDelay |
number |
120 |
Quiet period (ms) after the last scroll event before a user scroll settles, for engines without scrollend. |
Slots
| Slot |
Slot props |
Description |
default |
Record<string, never> |
PagerPage panels, in page order. |
Source: src/families/navigation/pager/pager-page.vue
Props
| Prop |
Type |
Default |
Description |
page (required) |
string |
required |
Page id this panel renders; must be one of the Pager pages. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly active: boolean } |
Page contents; stay mounted so swiping shows neighbors. |
Behavior
Normative state x input -> outcome table for pager.vue, pager-tab-list.vue,
pager-tab.vue, pager-viewport.vue, and pager-page.vue
(@vizejs/ui/pager): a swipeable, scroll-snap page view whose segmented tab bar
follows the WAI-ARIA APG tabs pattern with automatic activation. pages infers
the page-id union. Minimal consumer CSS: the viewport is display: flex; overflow-x: auto; scroll-snap-type: x mandatory and pages are flex: 0 0 100%; scroll-snap-align: start. Every row is proven by the named test in
pager.test.ts or pager-ssr.test.ts.
| # |
State |
Input |
Outcome |
Proven by |
| PG1 |
seeded |
render |
labelled tablist; selected tab is the roving tab stop wired to its tabpanel; off-screen panels are inert |
renders APG tabs wired to scroll-snap pages with inert off-screen panels |
| PG2 |
any |
tab click |
the page becomes active, the viewport smooth-scrolls to it, and change(page, previous, "tab") fires |
selecting a tab scrolls the viewport smoothly and emits typed changes |
| PG3 |
focused tablist |
Arrow / Home / End |
arrows (RTL-aware) wrap, Home/End jump; focus and selection move together |
arrow keys, Home, and End move the selected segment with wrapping |
| PG4 |
swiping |
scroll quiet period / scrollend |
the page nearest the scroll position becomes active with reason scroll; programmatic scrolls never re-select; next/previous stop at the ends |
user swipes settle on the nearest page while programmatic scrolls are ignored |
| PG5 |
controlled / reduced motion |
parent change |
controlled values win and parent changes scroll the viewport; reduced motion scrolls with behavior: "auto" |
controlled pages scroll when the parent changes them and reduced motion scrolls instantly |
| PG6 |
invalid setup |
empty pages / missing root |
throws VIZE_UI_PAGER_PAGES or VIZE_UI_CONTEXT_MISSING: Pager |
rejects empty page lists and parts outside a Pager |
| PG7 |
SSR / hydration |
isolated requests |
byte-identical tabs and panels; the initial page is scrolled into place only after hydration |
renders byte-identical tabs and pages and hydrates without mismatches |
The subpath ships no CSS.