Raccourci clavier : Ctrl + K
Commencer

Squelette de chargement

La silhouette d'un contenu qui n'est pas encore arrivé. Elle est en CSS pur, et décorative par défaut : ce qui annonce l'attente est le conteneur autour d'elle, pas une douzaine de silhouettes concurrentes.

Utilisation

vue
<script setup lang="ts">
import { VSkeletonLoader } from 'vectis-ui'
</script>

<template>
  <!-- The component has no width of its own: it fills what it is given, so a
       container is what decides how wide the silhouette is. -->
  <div class="demo">
    <VSkeletonLoader :lines="3" />
  </div>
</template>

<style scoped>
.demo {
  inline-size: 22rem;
}
</style>

Exemples

Formes

shape pose à la fois un rayon d'angle et une façon d'être dimensionné : text suit la typographie environnante, control prend la hauteur d'un contrôle, pill cette hauteur avec des extrémités complètement arrondies, circle cette hauteur dans les deux dimensions, surface une carte ayant sa propre hauteur. width et height en nomment une à eux, un nombre étant lu en pixels et tout le reste en longueur CSS.

vue

text

control

pill

circle

surface

width

height

<script setup lang="ts">
import { VSkeletonLoader, type SkeletonLoaderShape } from 'vectis-ui'

const shapes: SkeletonLoaderShape[] = ['text', 'control', 'pill', 'circle', 'surface']
</script>

<template>
  <div class="demo">
    <!-- Each shape sets a corner radius and a way of being sized: text follows the
         typography around it, control, pill and circle read the control scale, and a
         surface has a height of its own. -->
    <div v-for="shape in shapes" :key="shape" class="row">
      <p class="caption">{{ shape }}</p>
      <VSkeletonLoader :shape="shape" />
    </div>

    <!-- A number is read as pixels, anything else as a CSS length of your own. Left
         out, the silhouette takes all the width available, which is why a container
         has to give it one. -->
    <div class="row">
      <p class="caption">width</p>
      <VSkeletonLoader shape="control" :width="180" />
      <VSkeletonLoader shape="control" width="60%" />
    </div>

    <!-- `height` wins over the shape and the size, for a silhouette no shape
         describes. -->
    <div class="row">
      <p class="caption">height</p>
      <VSkeletonLoader shape="surface" :height="48" />
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-5);
  inline-size: 22rem;
}
.row {
  display: grid;
  gap: var(--vectis-space-2);
}
.caption {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Tailles

size reprend l'échelle partagée par tous les contrôles, de 24 à 56 pixels, et compact lui retire 4px. Elle ne veut rien dire pour text ni pour surface, dimensionnés autrement.

vue

circle

control

md, then md compact

<script setup lang="ts">
import { VSkeletonLoader, type SkeletonLoaderSize } from 'vectis-ui'

const sizes: SkeletonLoaderSize[] = ['xs', 'sm', 'md', 'lg', 'xl']
</script>

<template>
  <div class="demo">
    <!-- The control scale, 24 to 56 pixels: an md skeleton is exactly the height of
         an md button, which is what lets a silhouette hold the place of the control
         it stands in for. -->
    <div class="row">
      <p class="caption">circle</p>
      <div class="inline">
        <VSkeletonLoader v-for="size in sizes" :key="size" shape="circle" :size="size" />
      </div>
    </div>

    <div class="row">
      <p class="caption">control</p>
      <VSkeletonLoader v-for="size in sizes" :key="size" shape="control" :size="size" />
    </div>

    <!-- compact takes 4px off the height, as everywhere else in the design system. -->
    <div class="row">
      <p class="caption">md, then md compact</p>
      <VSkeletonLoader shape="control" size="md" />
      <VSkeletonLoader shape="control" size="md" compact />
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-5);
  inline-size: 18rem;
}
.row {
  display: grid;
  gap: var(--vectis-space-2);
}
.inline {
  display: flex;
  align-items: start;
  gap: var(--vectis-space-3);
}
.caption {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Paragraphes de texte

lines dessine autant de silhouettes, hautes d'un em avec l'interligne pour gouttière, si bien qu'elles occupent exactement ce nombre de lignes de la typographie environnante. La dernière est dessinée plus courte que les autres.

vue

body-sm, 5 lines

heading-2, 2 lines

<script setup lang="ts">
import { VSkeletonLoader } from 'vectis-ui'
</script>

<template>
  <div class="demo">
    <!-- In the text shape the height is one em and the gutter is the leading, so N
         silhouettes occupy exactly N lines of the typography around them: swapping
         them for the real text shifts nothing. The last line is drawn shorter, and
         that single detail is what reads as a paragraph rather than as a table. -->
    <div class="body">
      <p class="caption">body-sm, 5 lines</p>
      <VSkeletonLoader :lines="5" />
    </div>

    <div class="heading">
      <p class="caption">heading-2, 2 lines</p>
      <VSkeletonLoader :lines="2" />
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-6);
  inline-size: 24rem;
}
.body {
  font-size: var(--vectis-text-body-sm-size);
  line-height: var(--vectis-text-body-sm-leading);
}
.heading {
  font-size: var(--vectis-text-heading-2-size);
  line-height: var(--vectis-text-heading-2-leading);
}
.caption {
  margin: 0 0 var(--vectis-space-2);
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
  line-height: var(--vectis-text-caption-leading);
}
</style>

La silhouette d'un vrai composant

Rien n'est mesuré ici : une silhouette qui correspond au composant qu'elle remplace est une silhouette que vous avez déclarée, forme par forme.

vue

Component

Skeleton

Active filter

Sales analysis

The quarter closes on a 12% rise in revenue.

<script setup lang="ts">
import { VAvatar, VButton, VChip, VInput, VSkeletonLoader, VTypography } from 'vectis-ui'
</script>

<template>
  <div class="grid">
    <VTypography variant="overline" tone="muted" as="p">Component</VTypography>
    <VTypography variant="overline" tone="muted" as="p">Skeleton</VTypography>

    <!-- A control silhouette at the same size as the button, given the width the
         label would have taken. -->
    <VButton size="md">Save</VButton>
    <VSkeletonLoader shape="control" size="md" :width="128" />

    <VAvatar size="lg" name="Ada Lovelace" />
    <VSkeletonLoader shape="circle" size="lg" />

    <VChip size="xs">Active filter</VChip>
    <VSkeletonLoader shape="pill" size="xs" :width="88" />

    <!-- A field is two silhouettes: the label is a line of text, the box a control. -->
    <VInput size="md" label="Name" model-value="Ada Lovelace" />
    <div class="field">
      <VSkeletonLoader width="40%" />
      <VSkeletonLoader shape="control" size="md" />
    </div>

    <div class="card">
      <VTypography variant="heading-4">Sales analysis</VTypography>
      <VTypography variant="body-sm" tone="muted">
        The quarter closes on a 12% rise in revenue.
      </VTypography>
    </div>
    <div class="card">
      <VSkeletonLoader shape="control" size="sm" width="60%" />
      <VSkeletonLoader :lines="3" />
    </div>
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--vectis-space-6) var(--vectis-space-8);
  align-items: start;
  inline-size: 100%;
}
.field,
.card {
  display: grid;
  gap: var(--vectis-space-3);
}
.card {
  padding: var(--vectis-space-4);
  border: 1px solid var(--vectis-color-border);
  border-radius: var(--vectis-radius-surface);
}
</style>

Animations

animation envoie la lumière en vague à travers la silhouette, ou la lève et la baisse sur place en pulsation, et none la fige. Sous prefers-reduced-motion, la vague se rabat sur une pulsation ralentie.

vue

wave

pulse

none

<script setup lang="ts">
import { VSkeletonLoader, type SkeletonLoaderAnimation } from 'vectis-ui'

const animations: SkeletonLoaderAnimation[] = ['wave', 'pulse', 'none']
</script>

<template>
  <div class="demo">
    <!-- Both animations lighten the silhouette with the same highlight, derived from
         its own background: the wave sends it across, the pulse raises and lowers it
         in place. Neither fades the silhouette towards the page, which would lighten
         it in one theme and darken it in the other. `none` freezes it, for printing,
         a screenshot, or a parent that is already animating. -->
    <div v-for="animation in animations" :key="animation" class="row">
      <p class="caption">{{ animation }}</p>
      <VSkeletonLoader shape="surface" :animation="animation" />
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-5);
  inline-size: 22rem;
}
.row {
  display: grid;
  gap: var(--vectis-space-2);
}
.caption {
  margin: 0;
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
}
</style>

Une couleur à vous

color remplace le gris livré, la lumière qu'emploient les deux animations en étant dérivée par un écart de clarté plutôt que mélangée vers une cible.

vue
<script setup lang="ts">
import { VSkeletonLoader } from 'vectis-ui'
</script>

<template>
  <div class="demo">
    <!-- `color` replaces the background token. The highlight both animations use is
         derived from it by a lightness delta, so it stays right on an unusual surface
         and in either theme, with nothing else to set. -->
    <VSkeletonLoader shape="surface" color="oklch(55% 0.14 265)" />

    <!-- The case it is for: a card that paints its own ground, where the shipped grey
         would read as a hole. -->
    <div class="inverse">
      <VSkeletonLoader shape="control" size="sm" width="60%" color="oklch(38% 0.01 260)" />
      <VSkeletonLoader :lines="3" color="oklch(38% 0.01 260)" />
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-5);
  inline-size: 22rem;
}
.inverse {
  display: grid;
  gap: var(--vectis-space-3);
  padding: var(--vectis-space-4);
  border-radius: var(--vectis-radius-surface);
  background: var(--vectis-color-surface-inverse);
}
</style>

Remplacer le squelette

Il n'y a pas de mode enveloppe : l'idiome est un simple v-if, avec aria-busy sur le conteneur, qui est ce qui annonce l'attente pour toute la zone.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VSkeletonLoader, VTypography } from 'vectis-ui'

const pending = ref(true)
</script>

<template>
  <div class="demo">
    <VButton size="sm" variant="outline" tone="neutral" @click="pending = !pending">
      {{ pending ? 'Load' : 'Reload' }}
    </VButton>

    <!-- There is no wrapper mode: the component never measures what it replaces, so
         the shape is declared rather than guessed. The idiom is a plain v-if, with
         `aria-busy` on the container, which is what announces the wait for the whole
         zone instead of each silhouette announcing its own. -->
    <div class="zone" :aria-busy="pending || undefined">
      <template v-if="pending">
        <VSkeletonLoader shape="control" size="sm" width="55%" />
        <VSkeletonLoader :lines="3" />
      </template>
      <template v-else>
        <VTypography variant="heading-4">Sales analysis</VTypography>
        <VTypography variant="body-sm" tone="muted">
          The quarter closes on a 12% rise in revenue, driven by annual subscriptions.
        </VTypography>
      </template>
    </div>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-4);
  inline-size: 24rem;
}
.zone {
  display: grid;
  gap: var(--vectis-space-3);
  inline-size: 100%;
}
</style>

Annoncer l'attente

Une silhouette est décorative et masquée de l'arbre d'accessibilité par défaut. announce fait parler une instance, et label dit ce qui est annoncé et active l'annonce du seul fait d'être donné.

vue
Loading the results
<script setup lang="ts">
import { VSkeletonLoader } from 'vectis-ui'
</script>

<template>
  <div class="demo">
    <!-- A skeleton is silent by default: a page holds a dozen of them and a dozen
         competing announcements are unreadable. One instance per zone speaks, and
         `label` is what it says, which is also what turns the announcement on. Prefer
         something situated: a generic word is the reason the default is silence. -->
    <VSkeletonLoader shape="control" size="sm" label="Loading the results" />

    <!-- The rest stay decorative, hidden from the accessibility tree. -->
    <VSkeletonLoader v-for="n in 7" :key="n" shape="control" size="sm" />
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-2);
  inline-size: 20rem;
}
</style>

API

Props

PropTypeDéfaut
shapeSkeletonLoaderShape'text' | 'control' | 'pill' | 'circle' | 'surface''text'
Ce que la silhouette représente. Chaque valeur fixe à la fois un rayon de coin et une façon d'être dimensionnée : text suit la typographie alentour, control prend la hauteur d'un contrôle de la taille donnée, pill est cette hauteur aux extrémités entièrement arrondies, circle est cette hauteur dans les deux dimensions, et surface est une carte ou une image avec sa propre hauteur.
sizeSkeletonLoaderSize'xs' | 'sm' | 'md' | 'lg' | 'xl''md'
La taille sur l'échelle partagée par tous les contrôles. Elle ne signifie quelque chose que pour les formes dimensionnées comme un contrôle : le texte suit la typographie alentour, et une surface a sa hauteur propre.
compactbooleanfalse
Retire 4px à la hauteur, comme partout ailleurs dans le design system.
widthnumber | stringaucune
La largeur : un nombre est lu en pixels, et tout le reste comme une longueur CSS à vous, '100%' ou '12ch'. Sans elle, la silhouette prend toute la largeur disponible.
heightnumber | stringaucune
La hauteur, lue de la même façon. Elle l'emporte sur la forme et sur la taille.
linesnumber1
Combien de silhouettes empiler. Dans la forme texte, la dernière est dessinée plus courte que les autres, et ce seul détail est ce qui se lit comme un paragraphe plutôt que comme un tableau.
animationSkeletonLoaderAnimation'wave' | 'pulse' | 'none''wave'
Comment la silhouette montre qu'il se passe quelque chose. La couper la fige, ce qui convient à une impression, une capture d'écran, ou un parent qui anime déjà.
colorstringaucune
Une couleur de fond à vous, qui remplace le token. Le reflet de la vague en est dérivé, donc il reste juste sans rien d'autre à régler.
announcebooleanfalse
Annonce le chargement aux lecteurs d'écran. Désactivé par défaut, parce qu'un squelette est décoratif : une page en contient une douzaine, et une douzaine d'annonces concurrentes est illisible. Ce qui doit annoncer l'attente est le conteneur autour d'eux, marqué occupé.
labelstringaucune
Ce qui est annoncé, ce qui implique aussi de l'annoncer. Préférez quelque chose de situé, « Chargement des résultats », puisqu'un mot générique est la raison pour laquelle le silence est la valeur par défaut. Il retombe sur le dictionnaire du design system.

Variables CSS

TokenValeur
--vectis-control-size-skeleton-surface6rem