Aller au contenu
O-UI

Recherche

Trouvez un composant ou une page de documentation.

Shapeshift

BêtaComposant

Une transition entre images, textes ou thèmes par une onde de pièces qui se retournent.

Frameworks
ReactVueVanilla
Technologies
WebGLCanvas 2D
Déclenché par
PointeurContinuPiloté par l'application
Mis à jour le
Paramètres

Shapeshift fait passer un contenu d'une face à une autre sous une onde de petites pièces qui basculent, accrochent la lumière, puis se posent. Il couvre trois usages :

  • des images et du texte (mode image) : chaque pièce porte un morceau de la face, comme une écaille réversible ;
  • des sections, des composants d'interface et des mots (mode section) : le contenu reste du HTML vivant, révélé sous une bande de paillettes aux couleurs color et backColor ;
  • le thème de toute la page, avec la fonction shapeshiftTransition.

Réservez-le aux changements que l'on remarque : une galerie, un récit, une révélation, un thème. Pour un changement fréquent ou purement fonctionnel (onglets d'une interface de travail, filtres), un simple fondu enchaîné reste plus sobre.

Installation

Installez l'élément et ses dépendances en une commande :

pnpm dlx shadcn@latest add https://o-ui.hd987.dev/r/shapeshift.json

Shapeshift n'a aucune dépendance : il dessine avec WebGL2, disponible dans tous les navigateurs récents. Les prérequis communs à tous les composants (Tailwind CSS v4, React 19 ou Vue 3.5) sont décrits dans Installation.

Il installe avec lui les primitives FrameLoop, la boucle d'animation partagée par les animations d'O-UI, et PointerTracker, qui suit le pointeur pour toute la page. La fonction de page shapeshiftTransition est livrée dans le même dossier.

Utilisation

Chaque enfant direct de Shapeshift est une face. Changez index pour lancer la transition vers une autre face.

"use client";

import { useState } from "react";
import { Shapeshift } from "@/components/o-ui/shapeshift/shapeshift";

export function Cover() {
  const [face, setFace] = useState(0);
  return (
    <div className="grid gap-4">
      <Shapeshift index={face} onIndexChange={setFace} className="w-full max-w-2xl">
        <img src="/covers/dune.webp" alt="Dunes at dawn" className="aspect-video object-cover" />
        <img src="/covers/fjord.webp" alt="Snowy fjord" className="aspect-video object-cover" />
      </Shapeshift>
      <button type="button" onClick={() => setFace((face + 1) % 2)}>
        Next cover
      </button>
    </div>
  );
}

Shapeshift est un composant client. Cet exemple tient un état : son fichier commence donc par "use client".

Quelques règles font la différence :

  1. Une face, un enfant direct. Shapeshift empile les faces dans la même cellule de grille ; en React et en Vue, il enveloppe chaque enfant dans un div (data-shapeshift-face). Pour une face faite de plusieurs éléments (une carte, une grille de tarifs), regroupez-les dans un seul élément.
  2. Donnez aux faces la même taille. Le composant prend la taille de la plus grande face et ne bouge jamais à la bascule. Une face plus petite laisse un vide pendant la transition. Pour des images, gardez le même ratio et ajoutez object-cover.
  3. Pour remplir une boîte de hauteur fixe, visez les faces. En React et en Vue, chaque face est le div qui enveloppe votre élément : donnez à Shapeshift une hauteur et [&>[data-shapeshift-face]]:h-full, puis size-full object-cover aux images. En vanilla, les enfants directs sont les faces : donnez-leur directement size-full object-cover. Le journal de voyage remplit ainsi son cadre.
  4. Servez les images depuis votre domaine, ou avec CORS. En mode image, le navigateur interdit de dessiner une image d'un autre domaine sans autorisation : la transition devient alors un fondu. Ajoutez crossOrigin="anonymous" (crossorigin en HTML) et l'en-tête Access-Control-Allow-Origin côté serveur, ou passez en mode="section".
  5. Chargez vos polices tôt. En mode image, une face de texte est redessinée avec les polices de la page. Shapeshift attend leur chargement une seconde au plus : au-delà, la transition dessine le texte avec la police de repli. next/font et @nuxt/fonts préchargent déjà les leurs.
  6. En mode section, laissez de la place autour. La bande de paillettes déborde d’environ une pièce et demie autour du composant (36 px au plus), pour les pièces soulevées et leurs ombres (moins si le composant touche le bord du document). Elle est dessinée dans la racine : un overflow: hidden sur Shapeshift lui-même, ou sur un ancêtre, la coupe à son bord. En mode image, rien ne déborde : overflow-hidden rounded-* sur la racine convient pour arrondir l'image.

Exemples

Maison de vacances

Six miniatures pilotent les photos d'une location. Chacune appelle goTo(i, { origin: [x, 1] }) : l'onde monte du bas de l'image, juste au-dessus de la miniature choisie. origin se donne en fractions de 0 à 1 de la boîte ; un point hors de la boîte est ramené à son bord le plus proche.

Pour choisir l'origine, appelez goTo, et gardez index et onIndexChange pour que votre interface suive (miniature active, légende) : l'index que goTo vise est rapporté par onIndexChange, sans seconde transition.

Un mot qui change

Trois mots se relaient avec autoplay, trois secondes après chaque transition (interval={3}). Le mode section et origin="left" font traverser le mot par une bande de paillettes, de gauche à droite, sans redessiner les lettres. Le titre complet est donné aux lecteurs d'écran dans un texte masqué, et Shapeshift porte aria-hidden (voir Accessibilité).

De l'argile au four

Les cinq étapes d'un atelier de céramique : la photo se transforme d'une étape à l'autre. L'onde entre par le bord de la photo tourné vers l'étape choisie : le bord gauche, à la hauteur de l'étape, quand la liste est à côté ; le bord bas quand elle passe dessous, sur mobile.

Carte cadeau à révéler

Des paillettes dorées se retournent pour découvrir le montant et le code d'une carte cadeau, puis la recouvrent avec « Hide ». En mode="section", les deux faces restent du HTML : le bouton « Copy code » fonctionne normalement. color et backColor donnent l'or des deux côtés des paillettes, et l'onde part du bouton pressé.

Journal de voyage

Trois destinations se relaient toutes les six secondes avec autoplay. Les boutons appellent goTo avec une origine sous le bouton, et le nom de la destination suit l'index par un simple fondu enchaîné. La photo remplit le cadre grâce à [&>[data-shapeshift-face]]:h-full (règle 3) ; hover={0} garde les pièces immobiles sous le dégradé et les textes posés par-dessus.

Un journal, deux ambiances

Le bouton change la palette d'une mini page avec shapeshiftTransition : la nouvelle palette apparaît sous une bande de paillettes partie du bouton. En React, la mise à jour passe par flushSync ; en Vue, elle attend nextTick() (voir Thème de la page). Sur votre site, la transition couvre toujours toute la fenêtre : ici, elle semble contenue parce que l'aperçu tourne dans un cadre.

Personnalisation

Le mode

mode choisit la façon de dessiner la transition :

  • "image" : chaque pièce porte un morceau de la face. Pour des photos et du texte.
  • "section" : les deux faces restent du HTML vivant. La nouvelle apparaît sous une bande de paillettes, recto color, verso backColor. Pour n'importe quel contenu d'interface, texte compris.
  • "auto" (par défaut) choisit le mode image si toutes les faces peuvent être redessinées, et le mode section sinon. Une face ne peut pas l'être si elle contient un SVG, une vidéo, un canvas, une iframe, un champ de formulaire (input, textarea, select) ou une image de fond CSS (dégradés compris).

Le mode image redessine les images (avec object-fit et object-position), le texte (police, graisse, couleur, interlettrage), la couleur de fond et l'arrondi des éléments. Les ombres, les bordures et l'opacité CSS ne sont pas dessinées pendant l'onde, puis reviennent à la fin. Si vous forcez mode="image" sur une face qu'il ne sait pas redessiner, la transition devient un fondu.

Pour un texte, les deux modes conviennent. En mode image, les lettres se découpent en pièces qui basculent : chaque pièce porte un fragment des lettres. En mode section, une bande de paillettes color et backColor balaie le mot sans le redessiner, comme dans l'exemple Un mot qui change.

<Shapeshift mode="section" color="var(--primary)" backColor="var(--muted-foreground)">
  …
</Shapeshift>

color vaut currentColor par défaut : les pièces de la bande prennent la couleur du texte du composant. Toute couleur CSS convient, variables comprises.

La taille des pièces

size règle la largeur d'une pièce, de 6 à 64 pixels CSS. Avec "auto" (par défaut) :

  • en mode image, 2 % de la plus grande dimension de la boîte, entre 14 et 28 px ;
  • sur une face de texte, 0,14 fois la taille du texte, 8 px au moins ;
  • en mode section, 22 px.

Des pièces plus petites donnent une matière plus fine ; plus grandes, chaque bascule se lit mieux.

Le rendu

  • look, en mode image : "sequin" (par défaut) donne des paillettes brillantes, avec un reflet qui suit l'onde et de brefs éclats. "skin" donne un relief d'ombres plus discret : l'image reste lisible pendant toute l'onde. Le mode section utilise toujours le rendu sequin.
  • shine règle l'éclat des pièces, shadow leurs ombres portées et iridescence le reflet irisé de leur tranche.
  • shape choisit la forme : scale (écailles imbriquées), hex, square ou shard (éclats irréguliers).

Le rythme et l'irrégularité

duration règle la durée totale, en secondes ; chaque pièce bascule pendant un peu plus d'un tiers de cette durée. randomness règle l'irrégularité : le bord de l'onde ondule, les pièces partent et tournent à des vitesses un peu différentes, et des rafales de vent font frémir quelques groupes de pièces devant l'onde. À 0, l'onde est parfaitement régulière, sans vent. seed fixe le tirage : deux instances voisines avec la même seed bougent de la même façon.

L'origine

origin choisit le départ de l'onde :

  • "auto" (par défaut) part du dernier clic ou toucher dans le composant, s'il date de moins d'une seconde ; sinon, d'un bord ou d'un coin tiré au sort à chaque transition ;
  • "center" fait partir une onde ronde du centre ;
  • un bord ou un coin ("top", "bottom-left"…) fait partir une onde droite ;
  • un point [x, y], en fractions de 0 à 1 de la boîte, fait partir une onde ronde de ce point.

Une action peut aussi donner son origine, le temps d'une transition : goTo(2, { origin: [0.5, 1] }).

Le survol

En mode image, au repos, les pièces réagissent au pointeur : chacune est montée sur un ressort, et le geste les soulève selon sa vitesse et sa direction, avec un sillage derrière le pointeur. Un pointeur immobile les laisse se reposer. hover règle cette réaction ; hover={0} la coupe. Le survol n'existe qu'avec une souris ou un pavé tactile, jamais au toucher.

Combinaisons prêtes à copier

Discret, pour une section éditoriale sobre :

<Shapeshift look="skin" duration={1.4} randomness={0.2} shine={0.25} hover={0.2}>
  …
</Shapeshift>

Expressif, pour un hero :

<Shapeshift duration={3.2} randomness={0.6} shine={0.9} shadow={0.8} iridescence={0.5}>
  …
</Shapeshift>

Carrelage et éclats, pour un rendu plus graphique :

<Shapeshift shape="square" size={24} randomness={0.1}>…</Shapeshift>
<Shapeshift shape="shard" size={20}>…</Shapeshift>

Intégration

Next.js et Nuxt

Le rendu serveur montre la face index, sinon startIndex : rien ne bouge au chargement. Sans JavaScript, cette face reste affichée.

  • Next.js : Shapeshift est un composant client, utilisable dans un Server Component avec des props simples. Des fonctions (onIndexChange, onSettle) et actionsRef ne peuvent pas venir d'un Server Component : placez alors Shapeshift dans un composant à vous, marqué "use client". shapeshiftTransition s'appelle depuis un gestionnaire d'événement, donc dans un composant client.
  • Nuxt : aucun <ClientOnly> n'est nécessaire. Importez Shapeshift explicitement, comme dans les exemples.

Contrôler Shapeshift depuis l'application

Contrôlé ou non. startIndex choisit seulement la face de départ ; ensuite, Shapeshift avance par ses actions et son autoplay. index lie la face affichée à votre état, avec onIndexChange (v-model:index en Vue) :

  • index désigne la face, à partir de 0. Un index hors plage est ramené dans le nombre de faces : -1 désigne la dernière.
  • onIndexChange signale chaque nouvelle face visée, dès qu'elle est demandée, quelle qu'en soit la cause. onSettle (settle en Vue) signale la fin, quand la face d'arrivée est affichée.
  • Mettez votre état à jour dans onIndexChange. Sinon il garde l'ancienne valeur après un autoplay ou une action, et la redonner plus tard ne déclencherait rien.
  • Une demande faite pendant une transition attend la fin de l'onde en cours. Seule la dernière demande compte.

Des actions. En React, actionsRef reçoit next, prev, goTo, play et stop ; la ref reste l'élément racine. En Vue, la ref posée sur Shapeshift expose el et ces actions. next, prev et goTo acceptent { origin }, pour faire partir l'onde d'un point précis.

Les types. ShapeshiftActions et ShapeshiftOrigin s'importent de shapeshift.tsx ou de shapeshift-core.ts, et ShapeshiftTransitionOptions de shapeshift-transition.ts. En Vue, tous viennent de shapeshift-core.ts, que l'index du dossier réexporte. Typez la ref de Vue avec InstanceType<typeof Shapeshift>, comme dans les exemples : elle donne el et les actions.

"use client";

import { useRef, useState, type MouseEvent } from "react";
import { Shapeshift, type ShapeshiftActions } from "@/components/o-ui/shapeshift/shapeshift";

export function Collections() {
  const [index, setIndex] = useState(0);
  const shapeshift = useRef<ShapeshiftActions>(null);
  const panel = useRef<HTMLDivElement>(null);
  // Start the wave at the centre of the clicked button, in fractions of the panel.
  const show = (face: number) => (event: MouseEvent<HTMLButtonElement>) => {
    const box = panel.current?.getBoundingClientRect();
    const button = event.currentTarget.getBoundingClientRect();
    if (!box?.width || !box.height) return;
    shapeshift.current?.goTo(face, {
      origin: [
        (button.left + button.width / 2 - box.left) / box.width,
        (button.top + button.height / 2 - box.top) / box.height,
      ],
    });
  };
  return (
    <div className="grid gap-4">
      <div role="group" aria-label="Collection" className="flex gap-2">
        {["Spring", "Autumn"].map((name, face) => (
          <button key={name} type="button" aria-pressed={index === face} onClick={show(face)}>
            {name}
          </button>
        ))}
      </div>
      {/* index follows the face goTo targets, so the pressed button stays in step. */}
      <Shapeshift ref={panel} actionsRef={shapeshift} index={index} onIndexChange={setIndex}>
        <img src="/spring.webp" alt="The spring collection on a sunlit terrace" />
        <img src="/autumn.webp" alt="The autumn collection in a stone courtyard" />
      </Shapeshift>
    </div>
  );
}

Les actions ne dépendent d'aucun geste : appelez-les depuis un bouton, un raccourci clavier ou votre propre logique. Une action déclenchée au clavier n'a pas de point de clic : donnez-lui une origin, sinon l'onde part d'un bord.

Autoplay et pause

Avec autoplay, Shapeshift passe à la face suivante toutes les interval secondes (5 par défaut), en boucle. Le délai part de la fin de chaque transition.

  • Il attend, puis reprend : avec paused, avec la pause commune de la page, tant que le focus est dans le composant, hors de l'écran et dans un onglet masqué. Le délai repart là où il en était.
  • stop() l'arrête jusqu'à play() ou un changement de la prop autoplay. play() le lance, même sans la prop autoplay.
  • En mouvement réduit, il ne démarre pas. Seul play() le lance alors, avec des fondus de 0,2 s.

La pause commune de FrameLoop suspend l'autoplay de toutes les instances, et les autres animations continues d'O-UI : un seul interrupteur suffit pour la WCAG 2.2.2. Elle termine aussi aussitôt une transition en cours.

import { setAnimationsPaused } from "@/components/o-ui/frame-loop/frame-loop-core";

// From a "Pause animations" switch of the page:
setAnimationsPaused(true);

La recette complète de cet interrupteur, qui garde le choix du visiteur, est sur la page de FrameLoop.

Thème de la page

shapeshiftTransition(update, options) fait passer toute la page d'un état à un autre : thème ou langue. Elle capture la page, appelle update, puis révèle la nouvelle page sous une bande de paillettes. Elle renvoie une promesse, résolue à la fin de la transition ; une erreur levée dans update la rejette.

La règle d'or : update doit appliquer le changement au DOM avant de rendre la main, ou avant que sa promesse soit résolue. Sinon, la nouvelle capture montre encore l'ancien thème. En React, enveloppez la mise à jour dans flushSync ; en Vue, attendez nextTick().

"use client";

import { useTheme } from "next-themes";
import { flushSync } from "react-dom";
import { shapeshiftTransition } from "@/components/o-ui/shapeshift/shapeshift-transition";

export function ThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme();
  return (
    <button
      type="button"
      onClick={(event) => {
        // The button's centre: also right for a keyboard click, which has no pointer position.
        const rect = event.currentTarget.getBoundingClientRect();
        void shapeshiftTransition(
          () => {
            flushSync(() => {
              setTheme(resolvedTheme === "dark" ? "light" : "dark");
            });
          },
          { origin: [rect.left + rect.width / 2, rect.top + rect.height / 2] },
        );
      }}
    >
      Toggle theme
    </button>
  );
}

flushSync fait appliquer par React le nouveau rendu et ses effets avant de rendre la main : next-themes pose la classe du thème dans un tel effet.

  • origin est un point en pixels CSS de la fenêtre, ou un bord ou un coin nommé, qui fait partir une onde droite. Prenez le centre du bouton : il reste juste pour un clic au clavier. Sans origin, l'onde part du centre de l'écran.
  • La couleur des paillettes vient de la page, avant et après la mise à jour : le fond de la surface sous l'origine, c'est-à-dire l'élément opaque le plus proche qui couvre au moins la moitié de la fenêtre, sinon body ou html. Un bouton sous le pointeur ne prête pas sa couleur à la bande.
  • Un nouvel appel pendant une transition termine aussitôt la précédente, puis lance le sien : aucune bascule n'est perdue. Un redimensionnement de la fenêtre ou un onglet masqué termine aussi la transition en cours.
  • Sans View Transitions, update s'applique tout de suite, sans animation, et la promesse est résolue après la mise à jour.
  • En mouvement réduit ou en contraste élevé, la page change par le court fondu du navigateur, sans paillettes.
  • prepareShapeshiftTransition(), facultative, prépare l'effet à l'avance : appelez-la quand le pointeur arrive sur le bouton ou qu'il reçoit le focus (onPointerEnter et onFocus, ou @pointerenter et @focus). Le clic n'a alors plus à attendre cette préparation, et la transition démarre plus vite. L'effet préparé est gardé d'une transition à l'autre.

API

Shapeshift

PropTypeDéfautDescription
mode"auto" | "image" | "section""auto"`"image"` : chaque pièce porte un morceau de la face (photos, texte). `"section"` : le contenu reste du HTML vivant, révélé sous une bande de paillettes. `"auto"` choisit le mode image si toutes les faces peuvent être redessinées, le mode section sinon.
look"sequin" | "skin""sequin"Rendu des pièces en mode image : `"sequin"`, paillettes brillantes avec un reflet qui suit l'onde et de brefs éclats ; `"skin"`, relief d'ombres discret, l'image reste lisible. Le mode section utilise toujours le rendu `sequin`.
colorstring"currentColor"Mode section : couleur CSS du recto des pièces de la bande, variables comprises, relue au début de chaque transition. `currentColor` prend la couleur du texte du composant.
backColorstring"#c9cdd4"Mode section : couleur CSS du verso des pièces de la bande, relue au début de chaque transition. Un gris argent par défaut.
shapeShapeshiftShape"scale"Forme des pièces : `scale` (écailles imbriquées), `hex` (hexagones), `square` (carrés) ou `shard` (éclats irréguliers).
sizenumber | "auto""auto"Largeur d'une pièce, en pixels CSS, de 6 à 64. `"auto"` : 2 % de la plus grande dimension (de 14 à 28 px) en mode image, 0,14 fois la taille du texte (8 px au moins) sur une face de texte, 22 px en mode section. Plus petite, la matière est plus fine.
durationnumber2.2Durée totale de la transition, en secondes, de 0,4 à 8. Chaque pièce bascule pendant 36 % de cette durée. Plus courte, l'onde devient un balayage.
randomnessnumber0.35Irrégularité, de 0 à 1 : bord de l'onde qui ondule, départs et vitesses propres à chaque pièce, rafales de vent qui font frémir quelques groupes, variations du survol. À 0, l'onde est parfaitement régulière, sans vent.
seednumber1Graine du tirage, entier (une fraction est tronquée). Une même graine donne toujours la même onde ; donnez-en une différente à des instances voisines.
originShapeshiftOrigin"auto"Départ de l'onde : `"auto"`, `"center"`, un bord (`"top"`…), un coin (`"bottom-left"`…) ou un point `[x, y]` en fractions de 0 à 1 de la boîte. `"auto"` part du dernier clic dans le composant, sinon d'un bord ou d'un coin tiré au sort.
shinenumber0.5Éclat des pièces, reflet et liseré, de 0 à 1. À 0, elles basculent sans accrocher la lumière.
shadownumber0.5Densité des ombres portées par les pièces soulevées, de 0 (aucune) à 1.
iridescencenumber0.15Reflet irisé sur la tranche des pièces en mouvement, de 0 (aucun) à 1. Nul à plat : il ne colore jamais une face au repos.
hovernumber0.5Mode image : réaction des pièces au pointeur, au repos, de 0 à 1. À 0, le survol est coupé. Seulement avec une souris ou un pavé tactile, hors mouvement réduit et hors transition.
startIndexnumber0Face affichée au départ, à partir de 0, ramenée dans le nombre de faces. Seulement au premier rendu ; `index` l'emporte s'il est donné.
indexnumberundefinedFace affichée, contrôlée par l'application, à partir de 0 et ramenée dans le nombre de faces (`-1` désigne la dernière). Quand elle change, la transition part. Mettez-la à jour depuis `onIndexChange`.
autoplaybooleanfalseBooléen : passe seul à la face suivante toutes les `interval` secondes, en boucle. Ne démarre pas en mouvement réduit, sauf avec `play()`. Prévoyez la pause de la page (WCAG 2.2.2).
intervalnumber5Temps passé sur chaque face avec `autoplay`, en secondes, de 1 à 60, compté depuis la fin de la transition précédente.
pausedbooleanfalseBooléen : suspend l'autoplay, là où en est son délai. Pour toute la page, voir `setAnimationsPaused` de FrameLoop.
pixelRationumber2Densité de pixels maximale du rendu, de 0,5 à 2, limitée aussi par celle de l'écran et à 4 millions de pixels. Baissez-la sur un matériel modeste ou une très grande boîte.
disabledbooleanfalseBooléen : les faces changent aussitôt, sans transition ni survol, et le GPU est libéré. Le composant reste monté.
reducedMotionReducedMotion"user"`"user"` suit le réglage du système (fondu de 0,2 s, sans pièces ni survol, autoplay arrêté), `"always"` impose ce fondu, `"never"` garde l'effet complet : à réserver au cas où la personne a réactivé les animations dans les réglages de votre site.
onIndexChange((index: number) => void)undefinedAppelé avec la nouvelle face visée dès qu'elle est demandée, quelle qu'en soit la cause : `index`, autoplay ou action. `update:index` et `index-change` en Vue.
onSettle((index: number) => void)undefinedAppelé avec la face d'arrivée une fois la transition finie et la face affichée. `settle` en Vue.

ShapeshiftOrigin vaut "auto", "center", un bord, un coin ou un point [x, y].

Les nombres hors plage sont ramenés dans la plage ; seed, startIndex et index sont tronqués à l'entier. Une valeur invalide (NaN, mauvais type, valeur inconnue) garde la valeur courante, ou la valeur par défaut à la création. Une option changée après la création s'applique ainsi :

  • en direct : shine, shadow, iridescence et hover ;
  • à la transition suivante : look, shape, size, duration, randomness, seed et pixelRatio, ainsi que color et backColor, relues au début de chaque transition ;
  • aussitôt, en terminant la transition en cours : mode, et disabled ou reducedMotion quand ils coupent l'effet ;
  • pour l'autoplay : autoplay relance le délai depuis zéro et annule un stop() ; interval change la durée du délai en cours.

En React, className, style et les autres attributs natifs passent à la racine, un div, que désigne la ref ; actionsRef reçoit les actions next, prev, goTo, play et stop. En Vue, class et les attributs passent de la même façon ; la ref expose el et les mêmes actions, et les événements sont update:index, index-change et settle. En vanilla, createShapeshift(root, options) renvoie ces actions, plus update(options) et destroy().

shapeshiftTransition

shapeshiftTransition(update, options?) prend une fonction de mise à jour, synchrone ou asynchrone, et renvoie une promesse. Ses options sont propres à la fonction, avec leurs propres valeurs par défaut :

  • origin ("center"), shape ("scale") et seed (1) ;
  • size, de 6 à 32 pixels CSS ou "auto" (14), et randomness, de 0 à 0,6 (0.35) : au-delà, une pièce trop grande ou trop en avance ne couvrirait plus le bord de la révélation ;
  • duration, de 0,4 à 8 secondes (1.5) ;
  • shine (0.5), shadow (0.5), iridescence (0.15) et reducedMotion ("user").

Elle n'a ni color, ni backColor, ni look, ni hover, ni pixelRatio : la couleur de la bande vient de la page (voir Thème de la page). Pour préparer l'effet avant le clic, appelez prepareShapeshiftTransition().

PropTypeDéfautDescription
originShapeshiftTransitionOrigin"center"Départ de l'onde : un point `[x, y]` en pixels CSS de la fenêtre, ramené dans la fenêtre, ou un nom. `"center"` fait partir une onde ronde du centre de l'écran ; un bord (`"top"`…) ou un coin (`"bottom-left"`…) fait partir une onde droite.
shapeShapeshiftShape"scale"Forme des pièces de la bande : `scale` (écailles imbriquées), `hex` (hexagones), `square` (carrés) ou `shard` (éclats irréguliers).
sizenumber | "auto"14Largeur d'une pièce, en pixels CSS, de 6 à 32. `"auto"` prend 1,2 % de la plus grande dimension de la fenêtre, dans la même plage. Plus grande, la bande paraît plus grossière.
durationnumber1.5Durée totale de la transition, en secondes, de 0,4 à 8. Plus courte, la nouvelle page arrive en un balayage rapide ; plus longue, chaque pièce se voit basculer.
randomnessnumber0.35Irrégularité de l'onde, de 0 à 0,6. À 0, son bord avance en ligne parfaite ; la borne de 0,6 garde le bord de la révélation sous la bande.
seednumber1Graine du tirage, entier (une fraction est tronquée). Une même graine donne toujours la même onde.
shinenumber0.5Éclat des pièces de la bande, reflet et liseré, de 0 à 1. À 0, elles basculent sans accrocher la lumière.
shadownumber0.5Densité des ombres que les pièces soulevées portent sur la page, de 0 (aucune) à 1.
iridescencenumber0.15Reflet irisé sur la tranche des pièces en mouvement, de 0 (aucun) à 1.
reducedMotionReducedMotion"user"`"user"` suit le réglage du système, `"always"` remplace l'effet par le court fondu du navigateur, `"never"` garde l'effet complet : à réserver au cas où la personne a réactivé les animations dans les réglages de votre site.

ShapeshiftTransitionOrigin vaut "center", un bord, un coin ou un point [x, y] en pixels CSS de la fenêtre. Un nombre hors plage est ramené dans la plage. Une valeur invalide prend la valeur par défaut, puisque chaque appel part des réglages par défaut.

Accessibilité

Ce que fait Shapeshift

  • Les faces restent de vrais éléments HTML, avec leur sémantique (alt, titres, liens). Seule la face affichée est exposée : les autres sont masquées (visibility: hidden) et retirées du parcours au clavier et des lecteurs d'écran (aria-hidden, inert).
  • Si le focus est dans la face qui part, il passe dans la face qui arrive, sans faire défiler la page : sur son premier élément focalisable, sinon sur la face elle-même, rendue focalisable le temps qu'elle le garde.
  • Pendant une onde en mode image, la face d'arrivée est déjà exposée aux technologies d'assistance et cliquable : elle est transparente sous le canvas.
  • Le canvas de l'effet est décoratif (aria-hidden="true") et ne capte ni les clics ni le focus. Shapeshift n'a pas d'interaction clavier propre, et n'ajoute ni rôle ni tabindex, sauf ce tabindex temporaire.
  • Le survol est coupé au toucher, sur un pointeur sans survol et tant que le focus clavier est dans le composant.
  • L'autoplay attend avec paused, la pause commune de la page, tant que le focus est dans le composant, hors de l'écran et dans un onglet masqué. Il ne démarre pas en mouvement réduit. La pause commune termine aussi aussitôt une transition en cours.
  • Des faces ajoutées ou retirées après le montage sont prises en compte : Shapeshift observe ses enfants, et masque aussitôt une nouvelle face qui n'est pas affichée.

Ce que vous devez faire

  • Donner aux contrôles qui changent de face un nom accessible : un texte visible, ou un aria-label sur une miniature. Placez-les plutôt hors de Shapeshift ; un contrôle qui appartient à la face, comme « Reveal your gift » sur la carte cadeau, convient aussi : le focus passe alors dans la face d'arrivée. Les actions ne dépendent d'aucun geste : un bouton suffit pour le clavier.
  • Donner un texte de remplacement à chaque image qui informe.
  • Annoncer le changement quand il porte une information (une autre photo, un montant révélé) : une région aria-live="polite", mise à jour depuis onIndexChange, suffit, comme la légende de la galerie.
  • Pour un mot de titre qui change, donner le titre complet dans un texte masqué (sr-only) et poser aria-hidden sur Shapeshift, comme dans l'exemple : le titre se lit en entier et ne change pas sous le lecteur d'écran.
  • Avec autoplay, permettre de mettre la lecture en pause (WCAG 2.2.2) : un seul interrupteur pour toute la page, avec setAnimationsPaused de FrameLoop (voir Autoplay et pause). Le mouvement réduit du système ne le remplace pas.

Mouvement réduit. Les faces changent par un fondu de 0,2 s, sans pièces ni survol, et l'autoplay ne démarre pas : play() le lance quand même, avec ces fondus. shapeshiftTransition utilise le court fondu du navigateur. Un changement du réglage système s'applique en direct.

Contraste élevé. En mode forced-colors, Shapeshift ne dessine rien : les faces changent par un fondu, avec les couleurs du système.

Performances

Aucune dépendance. FrameLoop et PointerTracker sont partagés avec les autres éléments d'O-UI. Poids minifiés et compressés :

Ce que vous importezPoids
Le cœur et ses fichiers annexesenviron 20 kB (51 kB minifiés)
Le cœur, avec FrameLoop et PointerTracker22 kB
shapeshiftTransition seule, sans FrameLoopenviron 14 kB
Le composant, shapeshiftTransition et les primitives25 kB
  • Rien au repos. Les faces natives s'affichent et rien n'est dessiné. Le canvas n'apparaît que pendant une transition ou un survol. L'attente de l'autoplay est comptée par le navigateur, sans calcul à chaque image.
  • Un contexte WebGL par instance, préparé quand le navigateur est inoccupé et que le composant est à l'écran, et libéré quand il s'éloigne de plus de 200 px de l'écran. Toutes les instances partagent la boucle d'animation de FrameLoop.
  • Six contextes WebGL actifs au plus pour la page. Au-delà, les instances au repos utilisées le moins récemment rendent le leur, et le préparent de nouveau à leur prochaine transition.
  • Hors de l'écran ou dans un onglet masqué, rien ne tourne. Un changement de face demandé à plus de 200 px de l'écran s'applique aussitôt, sans transition.
  • Le coût d'une transition. Pendant le développement, sur un ordinateur portable Apple M5, une boîte de 1920 × 1200 pixels à densité 2 coûtait environ 9 ms par image avec les écailles. C'est un ordre de grandeur, pas une garantie : sur un matériel modeste ou une très grande boîte, baissez pixelRatio. La bande de shapeshiftTransition est dessinée à une densité de 1,25 au plus.
  • Un repli sur un GPU lent. Si, au début d'une transition, deux images sur quatre prennent plus de 120 ms, Shapeshift la termine par un fondu, et garde le fondu pour les transitions suivantes de cette instance.

Problèmes fréquents

  • La transition est un simple fondu. En mode image, une face contient une image d'un autre domaine servie sans CORS, une image pas encore chargée, ou un contenu que le mode image ne redessine pas alors que mode="image" est forcé. C'est aussi le cas en mouvement réduit, en contraste élevé, sans WebGL2 et sur un GPU trop lent. Servez les images avec CORS, ou passez en mode="section".
  • Le composant passe en mode section alors que vous attendiez des écailles. Une face contient un SVG (une icône), une vidéo, un canvas, une iframe, un champ de formulaire ou une image de fond CSS. Retirez-le de la face, ou forcez mode="image" si un fondu vous convient pour ces faces.
  • La première transition part avec un léger retard. En mode image, Shapeshift attend que les images soient décodées et les polices chargées, une seconde au plus. Préchargez vos polices, et servez des images aux dimensions affichées.
  • Le texte change de police pendant l'onde. La police n'était pas chargée après une seconde. Préchargez-la, avec next/font, @nuxt/fonts ou un <link rel="preload">.
  • Une ombre ou une bordure disparaît pendant l'onde. Le mode image ne les dessine pas. Placez-les sur un élément autour de Shapeshift, ou utilisez le mode section.
  • Avec next/image ou <NuxtImg>, la transition devient un fondu. Servies par /_next/image ou par le fournisseur de Nuxt Image, les images viennent de votre domaine : WebGL les accepte. Une face masquée chargée en différé peut en revanche ne pas être décodée à temps, et Shapeshift n'attend qu'une seconde. Chargez les images des faces tout de suite : loading="eager" (ou preload depuis Next.js 16) sur next/image, loading="eager" et preload sur <NuxtImg>. Une image en fill demande en plus une face positionnée et dimensionnée : combinez la règle 3 avec [&>[data-shapeshift-face]]:relative.
  • Le bord de la bande est coupé net. En mode section, la bande déborde d’environ une pièce et demie autour du composant. Un overflow: hidden sur Shapeshift lui-même ou sur un ancêtre la coupe : retirez-le, ou laissez cette marge à l'ancêtre.
  • Les faces changent sans transition. disabled est vrai, ou le composant était loin de l'écran. Un redimensionnement pendant une transition l'achève aussi aussitôt.
  • L'autoplay n'avance pas. Le mouvement réduit est actif (seul play() le lance alors), le focus est dans le composant, paused est vrai, la page a appelé setAnimationsPaused(true), stop() a été appelé, ou il n'y a qu'une face. Il attend aussi hors de l'écran et dans un onglet masqué.
  • Un index contrôlé ne déclenche plus rien. Votre état n'est pas mis à jour dans onIndexChange : il garde une valeur que le composant a déjà quittée.
  • La transition de page montre encore l'ancien thème. update a rendu la main avant que le DOM change. Enveloppez la mise à jour dans flushSync en React, ou attendez nextTick() en Vue.
  • Les paillettes de la transition de page sont blanches. Aucun élément opaque ne couvre la moitié de la fenêtre sous l'origine, et body comme html sont transparents. Posez le fond du thème sur la page entière.

Notes et limites

  • Navigateurs. Le composant demande WebGL2 et un Canvas 2D récent (roundRect, letterSpacing). Sans WebGL2, la transition est un fondu.
  • shapeshiftTransition repose sur les View Transitions : Chrome 111+, Safari 18+ et Firefox 144+. La révélation de la nouvelle page fonctionne dans ces trois navigateurs. Sans View Transitions, la mise à jour s'applique tout de suite.
  • La bande de paillettes de la page est vérifiée dans Chromium ; elle n'a pas encore été vérifiée dans le vrai Safari. Un navigateur qui ne la rafraîchit pas en direct montre seulement la révélation.
  • Images. En mode image, elles doivent venir de votre domaine ou être servies avec CORS : WebGL refuse les autres. Le mode section n'a pas cette limite.
  • Ce que le mode image redessine : les images (object-fit et object-position compris), le texte (polices, graisses, couleurs, interlettrage, casse), les couleurs de fond, les arrondis. Les ombres, les bordures et l'opacité ne sont pas dessinées pendant l'onde.
  • Une seule face : rien ne se passe. Un index hors plage est ramené dans le nombre de faces.
  • Taille. Un redimensionnement du composant pendant une transition saute à la face d'arrivée.