Shapeshift
BêtaComposantUne 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
- Construit sur
- FrameLoopPointerTrackerWebGLSurface
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 couleurscoloretbackColor; - 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.jsonShapeshift 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 :
- 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. - 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. - Pour remplir une boîte de hauteur fixe, visez les faces. En React et en Vue, chaque face est le
divqui enveloppe votre élément : donnez à Shapeshift une hauteur et[&>[data-shapeshift-face]]:h-full, puissize-full object-coveraux images. En vanilla, les enfants directs sont les faces : donnez-leur directementsize-full object-cover. Le journal de voyage remplit ainsi son cadre. - 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"(crossoriginen HTML) et l'en-têteAccess-Control-Allow-Origincôté serveur, ou passez enmode="section". - 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/fontet@nuxt/fontspréchargent déjà les leurs. - 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: hiddensur 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, rectocolor, versobackColor. 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 rendusequin.shinerègle l'éclat des pièces,shadowleurs ombres portées etiridescencele reflet irisé de leur tranche.shapechoisit la forme :scale(écailles imbriquées),hex,squareoushard(é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) etactionsRefne peuvent pas venir d'un Server Component : placez alors Shapeshift dans un composant à vous, marqué"use client".shapeshiftTransitions'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) :
indexdésigne la face, à partir de 0. Un index hors plage est ramené dans le nombre de faces :-1désigne la dernière.onIndexChangesignale chaque nouvelle face visée, dès qu'elle est demandée, quelle qu'en soit la cause.onSettle(settleen 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 propautoplay.play()le lance, même sans la propautoplay.- 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.
originest 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. Sansorigin, 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
bodyouhtml. 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,
updates'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 (onPointerEnteretonFocus, ou@pointerenteret@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
| Prop | Type | Défaut | Description |
|---|---|---|---|
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`. |
color | string | "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. |
backColor | string | "#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. |
shape | ShapeshiftShape | "scale" | Forme des pièces : `scale` (écailles imbriquées), `hex` (hexagones), `square` (carrés) ou `shard` (éclats irréguliers). |
size | number | "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. |
duration | number | 2.2 | Duré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. |
randomness | number | 0.35 | Irré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. |
seed | number | 1 | Graine 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. |
origin | ShapeshiftOrigin | "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. |
shine | number | 0.5 | Éclat des pièces, reflet et liseré, de 0 à 1. À 0, elles basculent sans accrocher la lumière. |
shadow | number | 0.5 | Densité des ombres portées par les pièces soulevées, de 0 (aucune) à 1. |
iridescence | number | 0.15 | Reflet irisé sur la tranche des pièces en mouvement, de 0 (aucun) à 1. Nul à plat : il ne colore jamais une face au repos. |
hover | number | 0.5 | Mode 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. |
startIndex | number | 0 | Face 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é. |
index | number | undefined | Face 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`. |
autoplay | boolean | false | Boolé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). |
interval | number | 5 | Temps passé sur chaque face avec `autoplay`, en secondes, de 1 à 60, compté depuis la fin de la transition précédente. |
paused | boolean | false | Booléen : suspend l'autoplay, là où en est son délai. Pour toute la page, voir `setAnimationsPaused` de FrameLoop. |
pixelRatio | number | 2 | Densité 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. |
disabled | boolean | false | Booléen : les faces changent aussitôt, sans transition ni survol, et le GPU est libéré. Le composant reste monté. |
reducedMotion | ReducedMotion | "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) | undefined | Appelé 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) | undefined | Appelé 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,iridescenceethover; - à la transition suivante :
look,shape,size,duration,randomness,seedetpixelRatio, ainsi quecoloretbackColor, relues au début de chaque transition ; - aussitôt, en terminant la transition en cours :
mode, etdisabledoureducedMotionquand ils coupent l'effet ; - pour l'autoplay :
autoplayrelance le délai depuis zéro et annule unstop();intervalchange 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") etseed(1) ;size, de 6 à 32 pixels CSS ou"auto"(14), etrandomness, 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) etreducedMotion("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().
| Prop | Type | Défaut | Description |
|---|---|---|---|
origin | ShapeshiftTransitionOrigin | "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. |
shape | ShapeshiftShape | "scale" | Forme des pièces de la bande : `scale` (écailles imbriquées), `hex` (hexagones), `square` (carrés) ou `shard` (éclats irréguliers). |
size | number | "auto" | 14 | Largeur 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. |
duration | number | 1.5 | Duré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. |
randomness | number | 0.35 | Irré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. |
seed | number | 1 | Graine du tirage, entier (une fraction est tronquée). Une même graine donne toujours la même onde. |
shine | number | 0.5 | Éclat des pièces de la bande, reflet et liseré, de 0 à 1. À 0, elles basculent sans accrocher la lumière. |
shadow | number | 0.5 | Densité des ombres que les pièces soulevées portent sur la page, de 0 (aucune) à 1. |
iridescence | number | 0.15 | Reflet irisé sur la tranche des pièces en mouvement, de 0 (aucun) à 1. |
reducedMotion | ReducedMotion | "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 nitabindex, sauf cetabindextemporaire. - 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-labelsur 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 depuisonIndexChange, 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 poseraria-hiddensur 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, avecsetAnimationsPausedde 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 importez | Poids |
|---|---|
| Le cœur et ses fichiers annexes | environ 20 kB (51 kB minifiés) |
| Le cœur, avec FrameLoop et PointerTracker | 22 kB |
shapeshiftTransition seule, sans FrameLoop | environ 14 kB |
Le composant, shapeshiftTransition et les primitives | 25 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 deshapeshiftTransitionest 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 enmode="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/fontsou 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/imageou<NuxtImg>, la transition devient un fondu. Servies par/_next/imageou 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"(oupreloaddepuis Next.js 16) surnext/image,loading="eager"etpreloadsur<NuxtImg>. Une image enfilldemande 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: hiddensur 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.
disabledest 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,pausedest 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.
updatea rendu la main avant que le DOM change. Enveloppez la mise à jour dansflushSyncen React, ou attendeznextTick()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
bodycommehtmlsont 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. shapeshiftTransitionrepose 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-fitetobject-positioncompris), 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.