Raccourci clavier : Ctrl + K
Commencer

Pagination

Une rangée de boutons de page. Chaque pastille est un VButton, donc rien de leurs états n'est redéfini ici, et la rangée peut abandonner des pages à mesure que la place se réduit, sans point de rupture.

Utilisation

vue
<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>

Exemples

Variantes et tonalités

itemVariant peint les pages qui ne sont pas courantes et les contrôles, en ghost par défaut ou en outlined. tone est la couleur que prend la page courante, le reste de la rangée restant neutre.

vue

ghost

outline

<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>

Comment la sélection est dessinée

selectedVariant est la façon dont la page courante est peinte dans le ton de la rangée : pleine en solid, teintée en soft, ou par la seule couleur de son texte en ghost. Dans une rangée outline, une page courante soft ou ghost garde la bordure du cadre, qui reste ainsi fermé.

vue

solid

soft

ghost

<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>

Détaché

detached espace les boutons et rend à chacun ses coins, au lieu de les joindre en contrôle segmenté. seamless les garde joints et retire les traits entre eux, si bien que la rangée se lit comme un cadre unique.

vue

Joined, the default

detached

seamless

<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>

Surélevé

elevated soulève la rangée. Jointe, l'ombre appartient à la rangée ; détachée, chaque bouton porte la sienne.

vue

elevated

elevated, detached

<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>

Tailles

size définit la hauteur, de 24 à 56 pixels, et compact lui retire 4px. Une pastille est carrée à un chiffre et s'élargit d'elle-même au-delà.

vue

xs

sm

md

lg

xl

xs compact

sm compact

md compact

lg compact

xl compact

<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>

Nombre de pages

length est le nombre total de pages, une par défaut. Toutes les pages sont rendues tant que totalVisible ne dit pas combien d'emplacements garder.

vue

length 4

length 12

<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>

Nombre d'emplacements

totalVisible est le nombre d'emplacements rendus par la rangée, points de suspension compris, ce qui garde sa largeur constante. La première et la dernière page sont toujours gardées, et cinq est le minimum utile.

vue

totalVisible 5

totalVisible 7

totalVisible 9

Page 1 of 20

<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>

Précédent et suivant

controls décide de ce que montrent les boutons précédent et suivant : une icône, un mot, les deux, ou rien du tout. Leurs icônes et leurs mots vous appartiennent, un libellé étant à la fois le texte visible et le nom accessible. Un bouton qui atteint la dernière page où il peut aller se désactive et passe le focus clavier à la page atteinte.

vue

icon, the default

text

both

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>

Pages inaccessibles

disabledPages accepte une liste de pages, ou une fonction quand la règle s'écrit plus facilement qu'elle ne s'énumère. Les contrôles précédent et suivant enjambent ces pages et ne se désactivent que lorsqu'il n'y a plus rien à atteindre.

vue

A list. Now on page 1

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>

États

disabled met toute la rangée hors de portée, grisée par les tokens de couleur. À chaque extrémité de la plage, le contrôle correspondant se désactive de lui-même.

vue

The whole row switched off

On the first page

On the last page

<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>

Alignement

align dit où se place la rangée dans la largeur qui lui est donnée. Il ne veut dire quelque chose qu'en mode responsive, qui fait prendre à la navigation toute la largeur disponible.

vue

start

center

end

<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>

Conteneurs étroits

responsive abandonne des pages à mesure que la place se réduit, en masquant les voisines de la page courante un cran à la fois, la première, la dernière et la courante ne partant jamais. Il mesure sa propre largeur et non celle de la fenêtre.

vue

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

PropTypeDéfaut
lengthnumber1
Combien de pages il y a en tout. Elle vaut 1 par défaut, ce qui rend une seule page : le vrai nombre doit presque toujours être donné.
totalVisiblenumberaucune
Combien d'emplacements rendre, ellipses comprises, pour que la rangée garde exactement la même largeur quelle que soit la page courante. En dessous de cinq il ne resterait rien à montrer autour de la page courante : cinq est donc le minimum effectif. Omise, toutes les pages sont rendues.
detachedbooleanfalse
Sépare les boutons au lieu de les joindre en un seul contrôle segmenté. C'est le mot que VButtonGroup et VToggle emploient pour la même question, dans le même sens.
seamlessbooleanfalse
Retire les traits entre les boutons joints, si bien que la rangée se lit comme un cadre unique plutôt que comme des segments, aux conditions de la prop de VButtonGroup. Sans effet sous detached.
itemVariantPaginationItemVariant'ghost' | 'outline''ghost'
Comment sont dessinées les pages autres que la page courante, ainsi que les contrôles. Ce que prend la page courante est selectedVariant. Elle porte le nom des items parce que c'est ce qu'elle peint : sur VTabs et VDataTable, variant nomme la décoration du cadre.
selectedVariantPaginationSelectedVariant'solid' | 'soft' | 'ghost''solid'
Comment est dessinée la page courante, dans le ton de la rangée : pleine, teintée, ou la seule couleur de son texte.
tonePaginationTone'accent' | 'neutral' | 'danger''accent'
La couleur que prend la page courante. Les autres pages et les contrôles restent neutres.
sizePaginationSize'xs' | 'sm' | 'md' | 'lg' | 'xl''md'
La hauteur des boutons, tirée de l'échelle partagée par tous les contrôles.
compactbooleanfalse
Retire 4px à la hauteur de chaque bouton.
elevatedbooleanfalse
Surélève la rangée. Jointe, l'ombre appartient à la rangée et non à chaque pastille, ce qui l'empêche de tomber dans les jointures ; détachée, chaque bouton porte la sienne.
alignPaginationAlign'start' | 'center' | 'end''start'
Où se place la rangée dans l'espace qu'on lui donne. Cela ne compte qu'en mode responsive, où la rangée prend toute la largeur disponible.
controlsPaginationControlsfalse | 'icon' | 'text' | 'both''icon'
Les boutons précédent et suivant de part et d'autre des pages : ce qu'ils affichent, ou false pour les retirer. Une prop plutôt que deux, la forme que preview de VFilePicker et controls de VCarousel emploient déjà.
prevIconIconSourcechevron_left
L'icône du contrôle précédent.
nextIconIconSourcechevron_right
L'icône du contrôle suivant.
prevTextstringaucune
Le libellé du contrôle précédent, utilisé à la fois comme texte visible et comme ce que les lecteurs d'écran annoncent. Il retombe sur le dictionnaire du design system.
nextTextstringaucune
Le libellé du contrôle suivant, utilisé à la fois comme texte visible et comme ce que les lecteurs d'écran annoncent. Il retombe sur le dictionnaire du design system.
disabledbooleanfalse
Rend tout le composant inutilisable.
disabledPagesPaginationMatcheraucune
Quelles pages ne peuvent pas être atteintes, en liste ou en fonction. Les contrôles précédent et suivant les ENJAMBENT plutôt que de s'arrêter dessus.
responsivebooleanfalse
Laisse la rangée abandonner des pages à mesure que la place se réduit, en interrogeant sa propre largeur. Désactivé par défaut, parce que cela fait prendre à la rangée toute la largeur disponible.
labelstringaucune
Ce que les lecteurs d'écran annoncent pour la navigation elle-même. Il retombe sur le dictionnaire du design system.
pageLabel(page: number) => stringaucune
Comment une page est annoncée. Une pastille affiche un nombre nu, qui seul ne signifie rien pour un lecteur d'écran : c'est ce qui en fait « Page 3 ». Il retombe sur le dictionnaire du design system.
v-modelnumber1
La page affichée, comptée à partir de 1. Elle démarre sur la première.

Types

Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.

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
export type PaginationMatcher = number[] | ((page: number) => boolean)