Raccourci clavier : Ctrl + K
Commencer

Tableau de données

Des lignes avec recherche, tri, sélection et pagination. Il fait les quatre lui-même sur les lignes qu'on lui donne, ou les confie à un serveur en se contentant de rapporter ce qui est demandé.

Utilisation

vue
Projects
ProjectOwnerCommits
Vectis UIAda Lovelace320
AtlasGrace Hopper87
MeridianAlan Turing44
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project' },
  { key: 'owner', label: 'Owner' },
  { key: 'commits', label: 'Commits' },
]

const rows = [
  { name: 'Vectis UI', owner: 'Ada Lovelace', commits: 320 },
  { name: 'Atlas', owner: 'Grace Hopper', commits: 87 },
  { name: 'Meridian', owner: 'Alan Turing', commits: 44 },
]
</script>

<template>
  <VDataTable :columns="columns" :rows="rows" row-key="name" title="Projects" />
</template>

Exemples

Tri

Une colonne marquée sortable reçoit un en-tête cliquable, qui alterne croissant, décroissant, puis l'ordre dans lequel les lignes ont été données. v-model:sort lit et pose cet état.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
BrumeLouisArchived1204
VectisXavierActive320
AtlasNadiaActive87
GranitEmmaActive45

Sorted by commits, descending

<script setup lang="ts">
import { ref } from 'vue'
import { VDataTable, type DataTableSort } from 'vectis-ui'

/* `sortable` turns a heading into a button and the table does the sorting itself, on the
   values as they are given. The ascending icon points down, the spreadsheet convention. */
const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status', sortable: true },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
  { name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
]

/* The sort is a model, so the table can open on a column already sorted, and what the
   reader clicks can be read back. */
const sort = ref<DataTableSort | null>({ key: 'commits', direction: 'desc' })

const DIRECTIONS = { asc: 'ascending', desc: 'descending' }
</script>

<template>
  <div class="demo">
    <VDataTable
      v-model:sort="sort"
      :columns="columns"
      :rows="rows"
      row-key="name"
      title="Projects"
      caption="Organisation projects"
    />
    <p class="state">
      Sorted by {{ sort ? `${sort.key}, ${DIRECTIONS[sort.direction]}` : 'nothing' }}
    </p>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-3);
}
.state {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-body-sm-size);
}
</style>

searchable pose un champ dans la barre d'outils, qui cherche dans les colonnes déclarées sans tenir compte de la casse ni des accents. v-model:search pilote le terme depuis ailleurs dans la page.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierActive320
AtlasNadiaActive87
BrumeLouisArchived1204
GranitEmmaActive45
ÉclairXavierActive296
FalaiseNadiaArchived133
GivreLouisActive58
HouleEmmaActive411
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', align: 'end' as const },
]

/* `Éclair` keeps its accent: the search ignores diacritics, so eclair finds it. */
const rows = [
  { name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
  { name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
  { name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
  { name: 'Falaise', owner: 'Nadia', status: 'Archived', commits: 133 },
  { name: 'Givre', owner: 'Louis', status: 'Active', commits: 58 },
  { name: 'Houle', owner: 'Emma', status: 'Active', commits: 411 },
]
</script>

<template>
  <VDataTable
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    searchable
    search-placeholder="Search projects"
    search-label="Search the projects"
    caption="Organisation projects"
  />
</template>

Pagination

Toute valeur de v-model:per-page supérieure à zéro active la pagination. showRange ajoute le compte des lignes à côté de la navigation.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierArchived111
AtlasNadiaActive148
BrumeLouisActive185
GranitEmmaArchived222
ÉclairXavierActive259
FalaiseNadiaActive296
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const NAMES = [
  'Vectis',
  'Atlas',
  'Brume',
  'Granit',
  'Éclair',
  'Falaise',
  'Givre',
  'Houle',
  'Islet',
  'Jade',
  'Karst',
  'Lande',
  'Mistral',
  'Nacre',
  'Ombre',
  'Pollen',
  'Quartz',
  'Rivage',
  'Sillage',
  'Tuile',
  'Vigie',
  'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']

const rows = NAMES.map((name, index) => ({
  name,
  owner: OWNERS[index % OWNERS.length],
  status: index % 3 === 0 ? 'Archived' : 'Active',
  commits: ((index + 3) * 37) % 500,
}))
</script>

<template>
  <!-- A page size is all it takes: passing one down, bound or not, turns the pagination on. -->
  <VDataTable
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    searchable
    :per-page="6"
    show-range
    caption="Organisation projects"
  />
</template>

Lignes par page

perPageOptions ajoute au pied un menu pour choisir le nombre de lignes par page. v-model:per-page rapporte ce qui a été choisi.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierArchived111
AtlasNadiaActive148
BrumeLouisActive185
GranitEmmaArchived222
ÉclairXavierActive259
<script setup lang="ts">
import { ref } from 'vue'
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const NAMES = [
  'Vectis',
  'Atlas',
  'Brume',
  'Granit',
  'Éclair',
  'Falaise',
  'Givre',
  'Houle',
  'Islet',
  'Jade',
  'Karst',
  'Lande',
  'Mistral',
  'Nacre',
  'Ombre',
  'Pollen',
  'Quartz',
  'Rivage',
  'Sillage',
  'Tuile',
  'Vigie',
  'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']

const rows = NAMES.map((name, index) => ({
  name,
  owner: OWNERS[index % OWNERS.length],
  status: index % 3 === 0 ? 'Archived' : 'Active',
  commits: ((index + 3) * 37) % 500,
}))

const perPage = ref(5)
</script>

<template>
  <VDataTable
    v-model:per-page="perPage"
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    :per-page-options="[5, 10, 25]"
    show-range
    caption="Organisation projects"
  />
</template>

Sélection

selectable ajoute une case à chaque ligne et une à l'en-tête pour la page visible. rowKey est alors obligatoire, et v-model:selected contient les identités données par ce champ.

vue
Projects
Organisation projects
Select every project on this pageProjectOwnerStatusCommits
VectisXavierActive320
AtlasNadiaActive87
BrumeLouisArchived1204
GranitEmmaActive45
ÉclairXavierActive296

Selected: Atlas

<script setup lang="ts">
import { ref } from 'vue'
import { VDataTable, type DataTableRowId } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
  { name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
  { name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
]

type Project = (typeof rows)[number]

/* What comes back are the identities `rowKey` names, never the row objects. */
const selected = ref<DataTableRowId[]>(['Atlas'])

/* "Select row" says nothing about which one, so the row itself names its checkbox. */
const selectRowLabel = (row: Project) => `Select ${row.name}`
</script>

<template>
  <div class="demo">
    <VDataTable
      v-model:selected="selected"
      :columns="columns"
      :rows="rows"
      row-key="name"
      title="Projects"
      selectable
      :select-row-label="selectRowLabel"
      select-all-label="Select every project on this page"
      caption="Organisation projects"
    />
    <p class="state">Selected: {{ selected.length ? selected.join(', ') : 'nothing' }}</p>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-3);
}
.state {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-body-sm-size);
}
</style>

Barre d'outils

Le slot #title remplace la prop title et occupe la gauche de la barre d'outils, le champ de recherche gardant la droite.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierActive320
AtlasNadiaActive87
BrumeLouisArchived1204
GranitEmmaActive45
ÉclairXavierArchived296
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VButton, VDataTable, VMenu, VMenuItem } from 'vectis-ui'
import { expand_more as expandMore } from 'vectis-ui/icons'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
  { name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
  { name: 'Éclair', owner: 'Xavier', status: 'Archived', commits: 296 },
]

/* The filtering is yours: the table shows the rows it is given, and the search field it
   provides narrows them further. */
const status = ref('All')
const filtered = computed(() =>
  status.value === 'All' ? rows : rows.filter((row) => row.status === status.value),
)
</script>

<template>
  <VDataTable
    :columns="columns"
    :rows="filtered"
    row-key="name"
    searchable
    caption="Organisation projects"
  >
    <template #title>
      <div class="toolbar">
        <span>Projects</span>
        <VMenu match-trigger>
          <template #trigger="{ triggerProps }">
            <VButton
              v-bind="triggerProps"
              variant="outline"
              tone="neutral"
              size="sm"
              :icon-end="expandMore"
            >
              Status: {{ status }}
            </VButton>
          </template>
          <VMenuItem
            v-for="option in ['All', 'Active', 'Archived']"
            :key="option"
            :label="option"
            @select="status = option"
          />
        </VMenu>
        <VButton size="sm">New project</VButton>
      </div>
    </template>
  </VDataTable>
</template>

<style scoped>
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--vectis-space-3);
}
</style>

Cellules personnalisées

Un slot nommé d'après la clé d'une colonne remplace le contenu de ses cellules, et reçoit la ligne, la valeur brute et la colonne. La recherche et le tri lisent toujours la valeur sous-jacente.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
Vectis Xavier DarmetActive320
Atlas Nadia RousseauActive87
Brume Louis FabreArchived1,204
Granit Emma LefortActive45
<script setup lang="ts">
import { VAvatar, VChip, VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier Darmet', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia Rousseau', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis Fabre', status: 'Archived', commits: 1204 },
  { name: 'Granit', owner: 'Emma Lefort', status: 'Active', commits: 45 },
]

const numbers = new Intl.NumberFormat('en-GB')
</script>

<template>
  <VDataTable
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    caption="Organisation projects"
  >
    <!-- A cell slot is named after its column key, and receives the whole row. -->
    <template #cell-owner="{ row }">
      <span class="owner">
        <VAvatar :name="row.owner" size="xs" />
        {{ row.owner }}
      </span>
    </template>

    <template #cell-status="{ row }">
      <VChip :tone="row.status === 'Active' ? 'success' : 'neutral'" size="xs">
        {{ row.status }}
      </VChip>
    </template>

    <!-- Sorting reads the underlying value, so a formatted number still sorts as a number. -->
    <template #cell-commits="{ row }">
      <span class="figure">{{ numbers.format(row.commits) }}</span>
    </template>
  </VDataTable>
</template>

<style scoped>
.owner {
  display: inline-flex;
  align-items: center;
  gap: var(--vectis-space-2);
}
.figure {
  font-variant-numeric: tabular-nums;
}
</style>

En-têtes personnalisés

Un slot nommé head- suivi de la clé de la colonne remplace un en-tête. Sur une colonne triable, il est rendu à l'intérieur du bouton de tri : tenez-vous-en donc au texte et à la décoration.

vue
Projects
Organisation projects
ProjectOwnerLast updateCommits
VectisXavier2 days ago320
AtlasNadia3 weeks ago87
BrumeLouisYesterday1204
GranitEmmaAn hour ago45
<script setup lang="ts">
import { VDataTable, VIcon } from 'vectis-ui'
import { code, schedule } from 'vectis-ui/icons'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'updated', label: 'Last update' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', updated: '2 days ago', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', updated: '3 weeks ago', commits: 87 },
  { name: 'Brume', owner: 'Louis', updated: 'Yesterday', commits: 1204 },
  { name: 'Granit', owner: 'Emma', updated: 'An hour ago', commits: 45 },
]
</script>

<template>
  <VDataTable
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    caption="Organisation projects"
  >
    <template #head-updated="{ column }">
      <span class="heading"><VIcon :name="schedule" :size="16" />{{ column.label }}</span>
    </template>

    <!-- On a sortable column the slot renders inside the sort button, so keep it to text
         and decoration: a control there would be a control inside a control. -->
    <template #head-commits="{ column }">
      <span class="heading"><VIcon :name="code" :size="16" />{{ column.label }}</span>
    </template>
  </VDataTable>
</template>

<style scoped>
.heading {
  display: inline-flex;
  align-items: center;
  gap: var(--vectis-space-1);
}
</style>

Variantes

variant définit la décoration : flat n'en porte aucune, outlined ajoute un fond surélevé, une bordure, des coins arrondis et une gouttière autour de la barre d'outils, de la légende et du pied.

vue
Flat
Organisation projects, flat
ProjectOwnerCommits
VectisXavier320
AtlasNadia87
BrumeLouis1204
Outlined
Organisation projects, outlined
ProjectOwnerCommits
VectisXavier320
AtlasNadia87
BrumeLouis1204
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', commits: 87 },
  { name: 'Brume', owner: 'Louis', commits: 1204 },
]
</script>

<template>
  <div class="stack">
    <!-- Flat carries no decoration at all and sits on whatever surface it is placed on. -->
    <VDataTable
      :columns="columns"
      :rows="rows"
      row-key="name"
      title="Flat"
      searchable
      caption="Organisation projects, flat"
    />

    <!-- Outlined makes it a card, and the frame gives the toolbar and the footer a gutter. -->
    <VDataTable
      variant="outlined"
      :columns="columns"
      :rows="rows"
      row-key="name"
      title="Outlined"
      searchable
      caption="Organisation projects, outlined"
    />
  </div>
</template>

<style scoped>
.stack {
  display: grid;
  gap: var(--vectis-space-6);
}
</style>

Compact

compact resserre chaque cellule, et avec elles le champ de recherche, le menu de taille de page et la pagination.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierActive320
AtlasNadiaActive87
BrumeLouisArchived1204
GranitEmmaActive45
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
  { name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
  { name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
  { name: 'Falaise', owner: 'Nadia', status: 'Archived', commits: 133 },
]
</script>

<template>
  <!-- The cells tighten, and so does everything the table renders with them: the search
       field, the page size menu and the pagination all take the shorter step. -->
  <VDataTable
    compact
    variant="outlined"
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    searchable
    :per-page="4"
    show-range
    caption="Organisation projects"
  />
</template>

Lignes zébrées

striped teinte une ligne sur deux.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierActive320
AtlasNadiaActive87
BrumeLouisArchived1204
GranitEmmaActive45
ÉclairXavierActive296
FalaiseNadiaArchived133
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
  { name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
  { name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
  { name: 'Falaise', owner: 'Nadia', status: 'Archived', commits: 133 },
]
</script>

<template>
  <VDataTable
    striped
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    caption="Organisation projects"
  />
</template>

stickyHeader garde les en-têtes de colonnes en place pendant le défilement des lignes. Il demande une zone de défilement bornée, par la prop height ou par un parent qui a sa propre hauteur.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierArchived111
AtlasNadiaActive148
BrumeLouisActive185
GranitEmmaArchived222
ÉclairXavierActive259
FalaiseNadiaActive296
GivreLouisArchived333
HouleEmmaActive370
IsletXavierActive407
JadeNadiaArchived444
KarstLouisActive481
LandeEmmaActive18
MistralXavierArchived55
NacreNadiaActive92
OmbreLouisActive129
PollenEmmaArchived166
QuartzXavierActive203
RivageNadiaActive240
SillageLouisArchived277
TuileEmmaActive314
VigieXavierActive351
ZenithNadiaArchived388
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const NAMES = [
  'Vectis',
  'Atlas',
  'Brume',
  'Granit',
  'Éclair',
  'Falaise',
  'Givre',
  'Houle',
  'Islet',
  'Jade',
  'Karst',
  'Lande',
  'Mistral',
  'Nacre',
  'Ombre',
  'Pollen',
  'Quartz',
  'Rivage',
  'Sillage',
  'Tuile',
  'Vigie',
  'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']

const rows = NAMES.map((name, index) => ({
  name,
  owner: OWNERS[index % OWNERS.length],
  status: index % 3 === 0 ? 'Archived' : 'Active',
  commits: ((index + 3) * 37) % 500,
}))
</script>

<template>
  <!-- The headings need something to stay put against, so the scrolling area has to be
       bounded: here by `height`, otherwise by a parent with a height of its own. -->
  <VDataTable
    sticky-header
    variant="outlined"
    :height="320"
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    caption="Organisation projects"
  />
</template>

Pleine hauteur

height borne tout le composant, barre d'outils et pied compris, un nombre étant lu en pixels. Sans elle, le tableau prend la hauteur de son parent dès que celui-ci en a une.

vue
Projects
Organisation projects
ProjectOwnerStatusCommits
VectisXavierArchived111
AtlasNadiaActive148
BrumeLouisActive185
GranitEmmaArchived222
ÉclairXavierActive259
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const NAMES = [
  'Vectis',
  'Atlas',
  'Brume',
  'Granit',
  'Éclair',
  'Falaise',
  'Givre',
  'Houle',
  'Islet',
  'Jade',
  'Karst',
  'Lande',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']

const rows = NAMES.map((name, index) => ({
  name,
  owner: OWNERS[index % OWNERS.length],
  status: index % 3 === 0 ? 'Archived' : 'Active',
  commits: ((index + 3) * 37) % 500,
}))
</script>

<template>
  <!-- The panel has a height, so the table takes it: the toolbar and the footer keep their
       places and only the rows scroll, whatever the page holds. Change the page size and
       the footer stays where it is. -->
  <div class="panel">
    <VDataTable
      variant="outlined"
      sticky-header
      :columns="columns"
      :rows="rows"
      row-key="name"
      title="Projects"
      searchable
      :per-page="5"
      :per-page-options="[5, 10]"
      show-range
      caption="Organisation projects"
    />
  </div>
</template>

<style scoped>
.panel {
  block-size: 26rem;
}
</style>

Conteneurs étroits

responsive décide de ce que fait un conteneur trop étroit pour les colonnes : défiler latéralement, ou transformer chaque ligne en carte avec ses en-têtes de colonnes répétés à l'intérieur. En cartes, la ligne d'en-têtes est hors de vue : ses boutons de tri et sa case « tout sélectionner » quittent l'ordre de tabulation avec elle.

vue
Projects
Organisation projects
Tout sélectionnerProjectOwnerStatusCommits
VectisXavierActive320
AtlasNadiaActive87
BrumeLouisArchived1204
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const rows = [
  { name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
  { name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
  { name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
]
</script>

<template>
  <!-- The threshold is the COMPONENT's own width, not the window's, so the box below is
       resizable: drag its corner past 640px and the cards become a table again. -->
  <div class="box">
    <VDataTable
      responsive="stack"
      variant="outlined"
      :columns="columns"
      :rows="rows"
      row-key="name"
      title="Projects"
      selectable
      caption="Organisation projects"
    />
  </div>
</template>

<style scoped>
.box {
  overflow: auto;
  inline-size: 24rem;
  min-inline-size: 15rem;
  max-inline-size: 100%;
  padding: var(--vectis-space-1);
  resize: horizontal;
}
</style>

Côté serveur

serverSide délègue la recherche, le tri et la pagination : les lignes sont affichées telles qu'elles arrivent et update:params rapporte chaque changement. Passez total pour la pagination et la plage, et searchDebounce pour retarder la recherche.

vue
Projects
Organisation projects, a page at a time
ProjectOwnerStatusCommits
Chargement des données…
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { VDataTable, type DataTableParams } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner' },
  { key: 'status', label: 'Status' },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const NAMES = [
  'Vectis',
  'Atlas',
  'Brume',
  'Granit',
  'Éclair',
  'Falaise',
  'Givre',
  'Houle',
  'Islet',
  'Jade',
  'Karst',
  'Lande',
  'Mistral',
  'Nacre',
  'Ombre',
  'Pollen',
  'Quartz',
  'Rivage',
  'Sillage',
  'Tuile',
  'Vigie',
  'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']

/* What a database would hold. Nothing here is ever handed to the table whole. */
const DATA = NAMES.map((name, index) => ({
  name,
  owner: OWNERS[index % OWNERS.length],
  status: index % 3 === 0 ? 'Archived' : 'Active',
  commits: ((index + 3) * 37) % 500,
}))

type Project = (typeof DATA)[number]

const rows = ref<Project[]>([])
const total = ref(0)
const loading = ref(true)

/* A stand-in for the request. In server mode the table filters, sorts and slices nothing,
   so everything below is the answering end's work. */
function load(params: DataTableParams) {
  loading.value = true

  window.setTimeout(() => {
    const term = params.search.trim().toLowerCase()
    const matching = term
      ? DATA.filter((row) => `${row.name} ${row.owner} ${row.status}`.toLowerCase().includes(term))
      : DATA

    const key = params.sortKey as keyof Project | null
    const sorted = key
      ? [...matching].sort(
          (a, b) =>
            String(a[key]).localeCompare(String(b[key]), 'en', { numeric: true }) *
            (params.sortDirection === 'desc' ? -1 : 1),
        )
      : matching

    const size = params.perPage ?? sorted.length
    const start = (params.page - 1) * size

    rows.value = sorted.slice(start, start + size)
    total.value = sorted.length
    loading.value = false
  }, 600)
}

/* Nothing is emitted when the table appears, so the first page is asked for here. */
onMounted(() => load({ page: 1, perPage: 5, sortKey: null, sortDirection: null, search: '' }))
</script>

<template>
  <VDataTable
    server-side
    variant="outlined"
    :columns="columns"
    :rows="rows"
    :total="total"
    :loading="loading"
    row-key="name"
    title="Projects"
    searchable
    :per-page="5"
    :per-page-options="[5, 10]"
    show-range
    caption="Organisation projects, a page at a time"
    @update:params="load"
  />
</template>

Chargement et vide

loading affiche un indicateur et loadingText à la place des lignes, et passe avant le vide. emptyText est ce que dit le tableau quand il n'y a rien à montrer. Les slots #loading et #empty remplacent l'un ou l'autre état, le second recevant la recherche (search) qui a vidé le tableau.

vue
Loading
Projects being loaded
ProjectOwnerCommits
Chargement des données…
Nothing to show
Projects, none of them
ProjectOwnerCommits
No project yet
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project' },
  { key: 'owner', label: 'Owner' },
  { key: 'commits', label: 'Commits', align: 'end' as const },
]
</script>

<template>
  <div class="stack">
    <!-- Loading is answered before emptiness, so a table waiting for its rows never
         claims there are none. -->
    <VDataTable
      loading
      variant="outlined"
      :columns="columns"
      :rows="[]"
      title="Loading"
      caption="Projects being loaded"
    />

    <VDataTable
      variant="outlined"
      :columns="columns"
      :rows="[]"
      empty-text="No project yet"
      title="Nothing to show"
      caption="Projects, none of them"
    />
  </div>
</template>

<style scoped>
.stack {
  display: grid;
  gap: var(--vectis-space-6);
}
</style>

Un tableau complet

Tout à la fois : un titre, une recherche, une sélection, quatre colonnes triables, des cellules personnalisées, et un pied portant le compte de la sélection, la taille de page, la plage et la pagination.

vue
Projects
Every project in the organisation
Tout sélectionnerProjectOwnerStatusCommits
Vectis Xavier DarmetArchived111
Atlas Nadia RousseauActive148
Brume Louis FabreActive185
Granit Emma LefortArchived222
Éclair Xavier DarmetActive259
Falaise Nadia RousseauActive296
Givre Louis FabreArchived333
Houle Emma LefortActive370
<script setup lang="ts">
import { ref } from 'vue'
import { VAvatar, VChip, VDataTable, type DataTableRowId } from 'vectis-ui'

const columns = [
  { key: 'name', label: 'Project', sortable: true },
  { key: 'owner', label: 'Owner', sortable: true },
  { key: 'status', label: 'Status', sortable: true },
  { key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]

const NAMES = [
  'Vectis',
  'Atlas',
  'Brume',
  'Granit',
  'Éclair',
  'Falaise',
  'Givre',
  'Houle',
  'Islet',
  'Jade',
  'Karst',
  'Lande',
  'Mistral',
  'Nacre',
  'Ombre',
  'Pollen',
  'Quartz',
  'Rivage',
  'Sillage',
  'Tuile',
  'Vigie',
  'Zenith',
]
const OWNERS = ['Xavier Darmet', 'Nadia Rousseau', 'Louis Fabre', 'Emma Lefort']

const rows = NAMES.map((name, index) => ({
  name,
  owner: OWNERS[index % OWNERS.length],
  status: index % 3 === 0 ? 'Archived' : 'Active',
  commits: ((index + 3) * 37) % 500,
}))

type Project = (typeof rows)[number]

const selected = ref<DataTableRowId[]>([])
const numbers = new Intl.NumberFormat('en-GB')

const selectRowLabel = (row: Project) => `Select ${row.name}`
const selectionText = (count: number) => `${count} project${count > 1 ? 's' : ''} selected`
</script>

<template>
  <VDataTable
    v-model:selected="selected"
    variant="outlined"
    :columns="columns"
    :rows="rows"
    row-key="name"
    title="Projects"
    searchable
    search-placeholder="Search projects"
    selectable
    :select-row-label="selectRowLabel"
    :selection-text="selectionText"
    sticky-header
    :height="420"
    :per-page="8"
    :per-page-options="[8, 16, 24]"
    show-range
    caption="Every project in the organisation"
  >
    <template #cell-owner="{ row }">
      <span class="owner">
        <VAvatar :name="row.owner" size="xs" />
        {{ row.owner }}
      </span>
    </template>

    <template #cell-status="{ row }">
      <VChip :tone="row.status === 'Active' ? 'success' : 'neutral'" size="xs">
        {{ row.status }}
      </VChip>
    </template>

    <template #cell-commits="{ row }">
      <span class="figure">{{ numbers.format(row.commits) }}</span>
    </template>
  </VDataTable>
</template>

<style scoped>
.owner {
  display: inline-flex;
  align-items: center;
  gap: var(--vectis-space-2);
}
.figure {
  font-variant-numeric: tabular-nums;
}
</style>

API

Props

PropTypeDéfaut
columnsDataTableColumn[]aucune
Les colonnes à afficher, dans l'ordre.
rowsRow[]aucune
Les lignes à afficher.
rowKeystringaucune
Quel champ identifie une ligne. Sans lui, une ligne est identifiée par sa position, ce qui suffit à l'affichage mais pas à une sélection : il doit être donné dès que des lignes peuvent être sélectionnées, sinon la sélection suit les positions et non les lignes.
captionstringaucune
Une phrase décrivant ce que contient le tableau. Elle est annoncée avant le tableau lui-même, et c'est elle qui dit à un utilisateur de lecteur d'écran si cela vaut la peine d'explorer.
variantDataTableVariant'flat' | 'outlined''flat'
Comment le tableau est encadré : rien du tout, ou une carte avec fond surélevé, bordure et coins arrondis.
responsiveDataTableResponsive'scroll' | 'stack''scroll'
Ce qui se passe quand le composant est trop étroit : le tableau défile latéralement, ou chaque ligne devient une carte avec ses en-têtes de colonne répétés à l'intérieur.
loadingbooleanfalse
Montre que les lignes sont en cours de chargement.
loadingTextstringaucune
Ce qui est écrit à côté de l'indicateur pendant le chargement des lignes. Il retombe sur le dictionnaire du design system.
emptyTextstringaucune
Ce qui est dit quand il n'y a aucune ligne à montrer. Il retombe sur le dictionnaire du design system.
titlestringaucune
Un titre au-dessus du tableau, à gauche de sa barre d'outils. Sans caption, il nomme aussi le tableau pour les lecteurs d'écran. Il masque l'attribut HTML du même nom sur le composant lui-même, compromis accepté : une infobulle sur tout un tableau serait de peu d'usage.
searchablebooleanfalse
Ajoute un champ de recherche à la barre d'outils.
searchPlaceholderstringaucune
Ce que dit ce champ quand il est vide. Il retombe sur le dictionnaire du design system.
searchLabelstringaucune
Ce que les lecteurs d'écran annoncent pour le champ de recherche. Il retombe sur le dictionnaire du design system.
searchDebouncenumber250
Quand un serveur fait la recherche, combien de temps attendre après une frappe avant de le solliciter, en millisecondes. Zéro le sollicite aussitôt.
stripedbooleanfalse
Teinte une ligne sur deux, ce qui aide l'œil à suivre une longue ligne à travers le tableau.
stickyHeaderbooleanfalse
Garde les en-têtes de colonne en place pendant que les lignes défilent dessous. Cela demande une zone de défilement bornée : soit la prop height, soit un parent ayant une hauteur propre.
compactbooleanfalse
Resserre les cellules d'un cran, et tout ce que le tableau rend avec elles.
heightnumber | stringaucune
La hauteur de TOUT le composant, barre d'outils et pagination comprises : un nombre est lu en pixels, tout le reste comme une longueur CSS. Omise, le tableau prend la hauteur de son parent dès que celui-ci en a une.
sortIconIconSourceswap_vert
L'icône d'en-tête d'une colonne triable mais qui ne l'est pas actuellement.
sortAscIconIconSourcearrow_downward
L'icône d'un tri croissant. Elle pointe vers le BAS par défaut, convention du tableur : trier de A à Z se lit vers le bas.
sortDescIconIconSourcearrow_upward
L'icône d'un tri décroissant.
perPageOptionsnumber[]aucune
Les choix proposés pour le nombre de lignes par page.
perPageTextstringaucune
Le nom de ce choix. Il retombe sur le dictionnaire du design system.
totalnumberaucune
Combien de lignes il y a en tout sur le serveur. C'est ce qui permet à la pagination et à la plage d'être justes quand le tableau ne détient jamais qu'une page.
showRangebooleanfalse
Affiche quelles lignes sont consultées, « 1 à 10 sur 42 », dans le pied.
rangeText(range: DataTableRange) => stringaucune
Reformule cette plage. Elle retombe sur le dictionnaire du design system.
selectablebooleanfalse
Ajoute une case à cocher à chaque ligne, et une dans l'en-tête pour prendre toute la page.
selectAllLabelstringaucune
Ce comme quoi la case d'en-tête est annoncée. Elle retombe sur le dictionnaire du design system.
selectionText(count: number) => stringaucune
Comment la sélection est résumée dans le pied. Elle ne dit rien du tout quand rien n'est sélectionné, et retombe sur le dictionnaire du design system.
selectRowLabel(row: Row, index: number) => stringaucune
Ce comme quoi la case d'une ligne est annoncée. « Sélectionner la ligne » ne dit rien à un utilisateur de lecteur d'écran sur QUELLE ligne : cela vaut donc la peine de fournir quelque chose venu de la ligne elle-même. index est la position de la ligne dans tout le tableau, à partir de 0, et non dans la page. Elle retombe sur le dictionnaire du design system, qui numérote les lignes à partir de 1.
serverSidebooleanfalse
Confie la recherche, le tri et la pagination à un serveur : les lignes sont affichées telles qu'elles arrivent, et chaque changement de ce qui est demandé est rapporté pour que le serveur puisse y répondre.
v-model:sortDataTableSort | nullnull
Selon quelle colonne les lignes sont triées, et dans quel sens. Rien n'est trié au départ. Cela peut être piloté de l'extérieur ou simplement laissé au tableau, qui le pose au clic sur les en-têtes ; le changer ne renvoie pas le lecteur à la première page.
v-model:pagenumber1
La page affichée, comptée à partir de 1. Rechercher ou changer la taille de page, depuis le menu ou de l'extérieur, y renvoie à la première. Elle est bornée par dérivation plutôt que réécrite : une page au-delà de la dernière affiche donc simplement la dernière.
v-model:perPagenumberaucune
Combien de lignes une page contient. Toute valeur supérieure à zéro active la pagination : en passer une sans la lier suffit donc à l'activer.
v-model:selectedDataTableRowId[][]
Les lignes sélectionnées, sous les identités que rowKey leur donne, jamais les objets de ligne eux-mêmes. Rien n'est sélectionné au départ, et une sélection SURVIT à un changement de page : la case d'en-tête ne couvre que la page visible, et c'est pourquoi elle peut être indéterminée. Le pied compte cette liste telle qu'elle est, identités de lignes qui ne sont plus affichées comprises.
v-model:searchstring''
Ce qui est saisi dans le champ de recherche, vide au départ. Seules les colonnes déclarées sont cherchées, sans tenir compte des accents ni de la casse ; en mode serveur rien n'est filtré ici et le terme est rapporté à la place.

Événements

ÉvénementType
update:params[params: DataTableParams]
Ce qui est demandé au tableau, en mode serveur : la recherche, le tri, la page et la taille de page. Il part à chaque changement et jamais au montage, et une valeur égale transmise à nouveau ne demande rien.

Slots

SlotType
title{}
Le côté gauche de la barre d'outils, qui remplace la prop title.
loading{}
Ce que montre le tableau pendant le chargement de ses lignes, à la place de l'indicateur et de son texte.
emptyDataTableEmptySlotProps
Ce que montre le tableau quand il n'a aucune ligne à montrer, à la place de emptyText. Il reçoit la recherche qui a produit ce résultat vide, vide si rien n'a été cherché.

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 interface DataTableColumn {
  key: string
  label: string
  sortable?: boolean
  align?: DataTableColumnAlign
}
export type DataTableColumnAlign = 'start' | 'center' | 'end'
export interface DataTableEmptySlotProps {
  search: string
}
export interface DataTableParams {
  page: number
  perPage: number | null
  sortKey: string | null
  sortDirection: DataTableSortDirection | null
  search: string
}
export interface DataTableRange {
  start: number
  end: number
  total: number
}
export type DataTableRowId = string | number
export interface DataTableSort {
  key: string
  direction: DataTableSortDirection
}
export type DataTableSortDirection = 'asc' | 'desc'
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

Variables CSS

TokenValeur
--vectis-control-size-table-search16rem