Pagination
A row of page buttons. Every pill is a VButton, so nothing about their states is redefined here, and the row can shed pages as the space narrows without a breakpoint.
Usage
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination } from 'vectis-ui'
const page = ref(1)
</script>
<template>
<VPagination v-model="page" :length="8" />
</template>Examples
Variants and tones
itemVariant paints the pages that are not current and the controls, ghost by default or outlined. tone is the colour the current page takes, the rest of the row staying neutral.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const variants = ['ghost', 'outline'] as const
const tones = ['accent', 'neutral', 'danger'] as const
const page = ref(3)
</script>
<template>
<div class="column">
<!-- The variant paints the OTHER pages and the controls; the current page takes
`selectedVariant`, filled by default. -->
<div v-for="variant in variants" :key="variant" class="row">
<VPagination
v-for="tone in tones"
:key="tone"
v-model="page"
:length="6"
:item-variant="variant"
:tone="tone"
:label="`${variant}, ${tone}`"
/>
<VTypography variant="caption" tone="muted">{{ variant }}</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-6);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>How the selection is drawn
selectedVariant is how the current page is painted in the row's tone: filled with solid, tinted with soft, or the colour of its text alone with ghost. In an outline row, a soft or ghost current page keeps the frame's border, so the frame stays closed.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography, type PaginationSelectedVariant } from 'vectis-ui'
const variants: PaginationSelectedVariant[] = ['solid', 'soft', 'ghost']
const pages = ref<Record<string, number>>({ solid: 3, soft: 3, ghost: 3 })
</script>
<template>
<div class="column">
<!-- How the current page is drawn, in the row's tone: filled with `solid`, tinted
with `soft`, or the colour of its text alone with `ghost`. Solid is the loudest
and the safest default; ghost is for a row that must stay quiet, where the
difference rests on the text colour alone. -->
<div v-for="variant in variants" :key="variant" class="row">
<VTypography variant="caption" tone="muted">{{ variant }}</VTypography>
<VPagination
v-model="pages[variant]"
:length="6"
:selected-variant="variant"
item-variant="outline"
:label="`Pages, ${variant}`"
/>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
justify-items: start;
gap: var(--vectis-space-5);
}
.row {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
</style>Detached
detached spaces the buttons out and gives each its own corners, instead of joining them into a segmented control. seamless keeps them joined and takes the lines out from between them, so the row reads as one frame.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const joined = ref(3)
const detached = ref(3)
const seamless = ref(3)
</script>
<template>
<div class="column">
<div class="row">
<VPagination v-model="joined" :length="6" item-variant="outline" label="Joined pages" />
<VTypography variant="caption" tone="muted">Joined, the default</VTypography>
</div>
<!-- The same word, in the same direction, as VButtonGroup and VToggle: the buttons
are spaced and each keeps its own corners. -->
<div class="row">
<VPagination
v-model="detached"
:length="6"
detached
item-variant="outline"
label="Detached pages"
/>
<VTypography variant="caption" tone="muted">detached</VTypography>
</div>
<!-- Joined again, with the lines between the buttons taken out: one frame, and the
current page as a highlight that moves inside it. -->
<div class="row">
<VPagination
v-model="seamless"
:length="6"
seamless
item-variant="outline"
label="Seamless pages"
/>
<VTypography variant="caption" tone="muted">seamless</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>Elevated
elevated raises the row off the page. Joined, the shadow belongs to the row; detached, every button carries its own.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const joined = ref(3)
const detached = ref(3)
</script>
<template>
<div class="column">
<!-- Joined, the shadow is the ROW's: one per pill would fall into every joint,
the segments overlapping by a pixel. -->
<div class="row">
<VPagination
v-model="joined"
:length="6"
elevated
item-variant="outline"
label="Raised joined pages"
/>
<VTypography variant="caption" tone="muted">elevated</VTypography>
</div>
<div class="row">
<VPagination
v-model="detached"
:length="6"
elevated
detached
item-variant="outline"
label="Raised detached pages"
/>
<VTypography variant="caption" tone="muted">elevated, detached</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-6);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>Sizes
size sets the height, from 24 to 56 pixels, and compact takes 4px off it. A pill is square at one digit and widens by itself past that.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
const page = ref(3)
</script>
<template>
<div class="column">
<div v-for="size in sizes" :key="size" class="row">
<VPagination
v-model="page"
:length="6"
:size="size"
item-variant="outline"
:label="`Pages, ${size}`"
/>
<VTypography variant="caption" tone="muted">{{ size }}</VTypography>
</div>
<!-- A pill is square at one digit and widens by itself past that: its minimum
width derives from the control height, so nothing is set per size. -->
<div v-for="size in sizes" :key="`${size}-compact`" class="row">
<VPagination
v-model="page"
:length="6"
:size="size"
compact
item-variant="outline"
:label="`Pages, ${size} compact`"
/>
<VTypography variant="caption" tone="muted">{{ size }} compact</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-4);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>Length
length is how many pages there are in all, one by default. Every page is rendered unless totalVisible says how many slots to keep.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const short = ref(2)
const long = ref(7)
</script>
<template>
<div class="column">
<div class="row">
<VPagination v-model="short" :length="4" item-variant="outline" label="Four pages" />
<VTypography variant="caption" tone="muted">length 4</VTypography>
</div>
<!-- Every page is rendered unless `totalVisible` says how many slots to keep. -->
<div class="row">
<VPagination v-model="long" :length="12" item-variant="outline" label="Twelve pages" />
<VTypography variant="caption" tone="muted">length 12</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>Total visible
totalVisible is how many slots the row renders, ellipses counted among them, which is what keeps its width constant. The first and last pages are always kept, and five is the effective minimum.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const page = ref(1)
</script>
<template>
<div class="column">
<!-- The count is CONSTANT: the ellipses are slots like the pages, so the row keeps
exactly the same width whichever page is current. Walk through it and nothing
moves sideways. -->
<div v-for="slots in [5, 7, 9]" :key="slots" class="row">
<VPagination
v-model="page"
:length="20"
:total-visible="slots"
item-variant="outline"
:label="`Twenty pages in ${slots} slots`"
/>
<VTypography variant="caption" tone="muted">totalVisible {{ slots }}</VTypography>
</div>
<VTypography variant="body-sm" tone="muted">Page {{ page }} of 20</VTypography>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>Previous and next
controls decides what the previous and next buttons show: an icon, a word, both, or nothing at all. Their icons and their wording are yours, a label being both the visible text and the accessible name. A control reaching the last page it can go to disables itself and hands the keyboard focus to the page just reached.
false, no controls at all
Icons and words of your own
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
import { arrow_left_alt as arrowLeftAlt, arrow_right_alt as arrowRightAlt } from 'vectis-ui/icons'
const DISPLAYS = [
{ value: 'icon', caption: 'icon, the default' },
{ value: 'text', caption: 'text' },
{ value: 'both', caption: 'both' },
{ value: false, caption: 'false, no controls at all' },
] as const
const page = ref(3)
</script>
<template>
<div class="column">
<!-- One prop rather than a boolean beside a union: you pick a member, you do not
turn one off. -->
<div v-for="display in DISPLAYS" :key="String(display.value)" class="row">
<VPagination
v-model="page"
:length="6"
:controls="display.value"
item-variant="outline"
:label="`Pages, controls ${display.caption}`"
/>
<VTypography variant="caption" tone="muted">{{ display.caption }}</VTypography>
</div>
<!-- The icons and the wording are yours. A label is both the visible text and the
accessible name, which is what keeps the control named once its text is hidden
at a narrow width. -->
<div class="row">
<VPagination
v-model="page"
:length="6"
controls="both"
:prev-icon="arrowLeftAlt"
:next-icon="arrowRightAlt"
prev-text="Newer"
next-text="Older"
item-variant="outline"
label="Pages with wording of their own"
/>
<VTypography variant="caption" tone="muted">Icons and words of your own</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>Unreachable pages
disabledPages takes a list of pages, or a function when the rule is easier to write than to enumerate. The previous and next controls step over those pages and disable themselves only when there is nothing left to step to.
A predicate. Now on page 1
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const listed = ref(1)
const predicate = ref(1)
</script>
<template>
<div class="column">
<!-- A list, for a handful of known pages. The controls STEP OVER them rather than
stopping at one: from page 1, next lands on 5. -->
<div class="row">
<VPagination
v-model="listed"
:length="10"
:disabled-pages="[2, 3, 4]"
controls="both"
item-variant="outline"
label="Pages 2 to 4 unavailable"
/>
<VTypography variant="caption" tone="muted">A list. Now on page {{ listed }}</VTypography>
</div>
<!-- A predicate, when the rule is easier to write than to enumerate. When nothing
is left to step to, the control disables itself. -->
<div class="row">
<VPagination
v-model="predicate"
:length="10"
:disabled-pages="(page) => page % 2 === 0"
controls="both"
item-variant="outline"
label="Even pages unavailable"
/>
<VTypography variant="caption" tone="muted">
A predicate. Now on page {{ predicate }}
</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>States
disabled puts the whole row out of reach, greyed through the colour tokens. At either end of the range the matching control disables itself.
The whole row switched off
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const page = ref(3)
const first = ref(1)
const last = ref(6)
</script>
<template>
<div class="column">
<div class="row">
<VPagination
v-model="page"
:length="6"
disabled
controls="both"
item-variant="outline"
label="Pages unavailable"
/>
<VTypography variant="caption" tone="muted">The whole row switched off</VTypography>
</div>
<!-- The controls disable themselves at the ends: there is nothing to step to. -->
<div class="row">
<VPagination
v-model="first"
:length="6"
controls="both"
item-variant="outline"
label="On the first page"
/>
<VTypography variant="caption" tone="muted">On the first page</VTypography>
</div>
<div class="row">
<VPagination
v-model="last"
:length="6"
controls="both"
item-variant="outline"
label="On the last page"
/>
<VTypography variant="caption" tone="muted">On the last page</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-5);
}
</style>Alignment
align says where the row sits in the width it is given. It only says something in responsive mode, which is what makes the nav take the whole width available.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const aligns = ['start', 'center', 'end'] as const
const page = ref(3)
</script>
<template>
<div class="column">
<!-- Alignment only says something in responsive mode, where the nav takes the whole
width it is given. Outside it the row has its own intrinsic width and sits
wherever its parent puts it. -->
<div v-for="align in aligns" :key="align" class="box">
<VPagination
v-model="page"
:length="6"
responsive
:align="align"
item-variant="outline"
:label="`Pages aligned ${align}`"
/>
<VTypography variant="caption" tone="muted">{{ align }}</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
.box {
display: grid;
gap: var(--vectis-space-2);
padding: var(--vectis-space-3);
border: 1px dashed var(--vectis-color-border);
border-radius: var(--vectis-radius-surface);
}
</style>Narrow containers
responsive sheds pages as the space narrows, hiding the neighbours of the current page one step at a time while the first page, the last and the current one never go. It measures its own width rather than the window's.
Drag the handle at the corner: the neighbours of the current page go one step at a time, while the first page, the last and the current one never do.
<script setup lang="ts">
import { ref } from 'vue'
import { VPagination, VTypography } from 'vectis-ui'
const page = ref(8)
</script>
<template>
<div class="column">
<VTypography variant="body-sm" tone="muted">
Drag the handle at the corner: the neighbours of the current page go one step at a time, while
the first page, the last and the current one never do.
</VTypography>
<!-- The row asks about its OWN width rather than the window's, so it sheds pages
inside a narrow panel while the page around it stays wide. -->
<div class="box">
<VPagination
v-model="page"
:length="16"
responsive
controls="both"
align="center"
item-variant="outline"
label="Sixteen pages in a narrow panel"
/>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-3);
}
.box {
overflow: auto;
inline-size: 38rem;
min-inline-size: 14rem;
max-inline-size: 100%;
padding: var(--vectis-space-3);
border: 1px dashed var(--vectis-color-border);
border-radius: var(--vectis-radius-surface);
resize: horizontal;
}
</style>API
Props
Types
The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.
export interface BuiltinIcon {
name: string
paths: readonly [string] | readonly [string, string]
}export type IconRender =
| { path: string; viewBox?: string }
| { component: Component; props?: Record<string, unknown> }
| { src: string }
| { text: string; class?: string }
| { class: string }export type IconSource = string | BuiltinIcon | IconRender