Aller au contenu
O-UI

Recherche

Trouvez un composant ou une page de documentation.

PhaseDriver

BêtaPrimitive

Une phase de 0 à 1, sans couture, qui avance avec le temps, le défilement ou l'application : une animation, trois façons de la lire.

Frameworks
ReactVueVanilla
Mis à jour le
Utilisée par
ProjectedText
Paramètres

PhaseDriver donne à une animation une seule source de temps : une phase de 0 à 1. Vous calculez votre image à partir de la phase seule, et la phase 1 rejoint la phase 0 : la boucle est sans couture. Vous choisissez ensuite, sans rien réécrire, si l'animation tourne en boucle, suit le défilement ou obéit à votre application. Pour une animation qui se joue une seule fois, ou qui dépend d'un état accumulé (une simulation), préférez FrameLoop.

Dans l'aperçu, l'aiguille et sa traîne, l'onde des barres et la valeur lisent la même phase. Passez en mode « Pilotée » pour la régler à la main.

Installation

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

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

PhaseDriver s'appuie sur FrameLoop et ViewProgress, installés avec lui dans components/o-ui/. Les exemples utilisent Tailwind CSS v4, comme tous les éléments d'O-UI (prérequis).

Utilisation

Une bande rayée avance d'une période par boucle, par une transformation. La phase arrive dans une fonction, qui écrit directement dans le DOM :

"use client";

import { useRef } from "react";
import { usePhaseDriver } from "@/components/o-ui/phase-driver/phase-driver";

export function Stripes() {
  const ref = useRef<HTMLDivElement>(null);
  // One stripe period (24px) per loop, moved by a transform: the end meets the start.
  usePhaseDriver(ref, (phase) => {
    ref.current?.style.setProperty("translate", `${-phase * 24}px 0`);
  });
  return (
    <div className="h-3 overflow-hidden rounded-full">
      <div
        ref={ref}
        className="h-full w-[calc(100%+24px)] bg-[repeating-linear-gradient(90deg,var(--primary)_0_12px,transparent_12px_24px)]"
      />
    </div>
  );
}

Les règles

  1. L'image ne dépend que de la phase. Pas d'état accumulé d'une image à l'autre : c'est ce qui permet de passer de la boucle au défilement sans rien changer, et de remonter la page à reculons.
  2. La phase 1 doit donner la même image que la phase 0. Déplacez un nombre entier de périodes (une rayure, un tour, une rangée) : la boucle se ferme sans saut.
  3. Écrivez dans la fonction, pas dans l'état de votre framework. Elle est appelée dans la phase d'écriture de FrameLoop, après toutes les lectures de la page, et seulement quand la phase change : rien ne se rend de nouveau à chaque image. Animez transform et opacity : une autre propriété (background-position, une couleur) repeint à chaque image.
  4. Choisissez une phase de pose (posterPhase) qui fait une belle image fixe : c'est elle qui s'affiche sous mouvement réduit, en boucle et au défilement, et c'est le départ de la boucle. La changer fait repartir la boucle de la nouvelle valeur.

Les trois sources

driveLa phase vient de…S'arrête…
"loop"Le temps : une boucle de duration secondes.Hors écran, dans un onglet masqué, avec paused et la pause commune
"scroll"La progression de l'élément sur range, multipliée par cycles.Quand on ne fait plus défiler ; remonter la fait reculer
"manual"progress, que fixe votre application, sous mouvement réduit aussi (info.reduced vous laisse adoucir son rendu).Quand progress ne change plus

Exemples

Progression indéterminée

Les rayures d'une barre de déploiement avancent d'une période par boucle de 1,2 s : la fin rejoint le début, sans à-coup. La boucle s'arrête hors écran et avec la pause commune de la page, sans une ligne de plus.

Cadran épinglé au défilement

Le cadran reste en place (position: sticky) pendant que la section défile. Un élément épinglé ne bouge pas dans la vue : sa phase suit donc la section qui le contient, désignée par scrollSubject: "section". Sur la plage contain et deux cycles, l'aiguille fait deux tours et revient à midi ; remontez, elle repart en arrière.

Timeline d'animation

En lecture, la boucle de 3 s fait avancer la phase et le curseur la suit. En pause, drive passe à "manual" et le curseur fixe progress. La pause part de phase, lu sur le handle dans le gestionnaire du bouton : l'image reste en place, et la lecture repart de là où on l'a laissée. Sous mouvement réduit, la lecture montre l'image de pose, et le curseur reste libre.

Intégration

Passer de la boucle au défilement

La même animation change de source par une seule option. Une bascule de l'application, un réglage par page, ou la préférence d'un visiteur suffisent :

driver.update({ drive: "scroll", cycles: 2, range: "contain" });

Entrer dans la boucle repart de l'image affichée ; changer duration garde l'image et change seulement la vitesse.

Redessiner sans changer de phase

Après un redimensionnement ou un changement de couleur, appelez refresh() : la fonction est rappelée à la prochaine image avec la phase courante. Les hooks et les composables renvoient un handle qui expose refresh() et phase, à lire dans un gestionnaire d'évènement, pas au rendu.

Une scène épinglée

Pour une section haute dont le contenu reste épinglé, donnez à l'élément animé un sélecteur de son ancêtre : scrollSubject: "section", "[data-scene]"… Il est cherché à partir du parent, avec closest() ; sans correspondance, l'élément lui-même sert de sujet. La plage contain couvre le temps où la section remplit la vue ; cover, son entrée et sa sortie en plus.

Next.js et Nuxt

Les hooks commencent par "use client" et n'agissent qu'au montage ; les composables, dans onMounted. Au rendu serveur, rien n'est mesuré ni appelé : votre HTML doit montrer l'image de posterPhase par lui-même (une variable CSS posée dans le markup, comme [--phase:0] dans les démos de cette page). Avant que le pilote existe, le handle renvoie posterPhase, ramenée dans [0, 1).

API

Le cœur (phase-driver-core.ts)

ExportRôle
observePhase(element, onPhase, options)Appelle onPhase(phase, info) dans la phase d'écriture, à la première image puis à chaque changement. Renvoie une instance.
wrapPhase(value)Ramène un nombre dans [0, 1) ; 1 devient 0, la même image d'une boucle sans couture.
PHASE_DRIVER_DEFAULTSLes valeurs par défaut des options.

L'instance expose phase (la phase courante), update(options), refresh() et destroy(), sans effet après la destruction. info donne drive et reduced (le mouvement réduit s'applique : la phase est posterPhase).

Les hooks et les composables

FonctionRôle
usePhaseDriver(target, onPhase, options)Crée le pilote quand l'élément existe, le recrée s'il change, le détruit au démontage. Renvoie un handle stable { phase, refresh }.

En React, target est une ref ou l'élément ; une option retirée revient à sa valeur par défaut, et la fonction onPhase est relue à chaque rendu. En Vue, target est une ref de template ou un getter, et options un objet, une ref ou un getter ; onPhase est lue une seule fois, à l'appel du composable ; <KeepAlive> arrête le pilote pendant la désactivation.

Les options (PhaseDriverOptions)

PropTypeDéfautDescription
drivePhaseDrive"loop"La source de la phase : "loop" (le temps), "scroll" (le défilement) ou "manual" (progress). Passez de l'une à l'autre à tout moment.
durationnumber4La durée d'une boucle, en secondes, de 0,1 à 600. La changer garde l'image affichée.
cyclesnumber1Le nombre de boucles sur la plage de défilement, de 0 à 100. Un nombre entier finit sur l'image de départ.
rangeViewRangeShorthand"cover"La plage de défilement, comme animation-range en CSS : "cover" (de l'entrée à la sortie de l'élément), "contain" (tant qu'il remplit la vue), "entry 25% exit 75%"…
scrollSubjectstring""Un sélecteur CSS de l'ancêtre dont le défilement mène la phase, cherché à partir du parent (closest()) : une section haute autour d'un contenu épinglé. Vide, ou sans ancêtre correspondant : l'élément lui-même.
progressnumber0La phase en mode manual, de 0 à 1 ; 1 donne la même image que 0.
posterPhasenumber0La phase affichée sous mouvement réduit (boucle et défilement), et le départ de la boucle, de 0 à 1. La changer fait repartir la boucle. Choisissez une image fixe lisible.
reducedMotion"always" | "never" | "user""user""user" suit la préférence du système ; "always" la force. Réduit, la boucle et le défilement restent sur posterPhase, et le mode manual suit toujours progress. "never" garde le mouvement : à réserver au cas où la personne a réactivé les animations dans les réglages de votre site.
pausedbooleanfalseArrête la boucle. Sans effet au défilement et en mode manual.
disabledbooleanfalseArrête tout, sans détruire le pilote : plus de mesure ni d'appel, la phase garde sa valeur.

Une valeur hors plage est bornée ; une valeur invalide (NaN, un mauvais type, un sélecteur invalide) garde la valeur courante.

Accessibilité

  • Ce que fait PhaseDriver : la boucle obéit à la pause commune de la page (setAnimationsPaused, WCAG 2.2.2) et à paused, et s'arrête hors écran. Sous mouvement réduit, la boucle et le défilement restent sur posterPhase, et la boucle repart de cette image quand la préférence est levée, en direct. Le mode manual suit toujours progress : c'est l'état de votre application, pas un mouvement ambiant. Au défilement, la phase n'avance que si le visiteur fait défiler : le critère 2.2.2 ne s'y applique pas.
  • Ce que vous faites :
    • dessinez une belle image fixe pour posterPhase (jamais un écran vide), gardez un contenu lisible sans JavaScript, et marquez aria-hidden ce qui n'est que décoratif ;
    • pour une animation continue de plus de cinq secondes, reliez un interrupteur de pause à setAnimationsPaused (FrameLoop) ;
    • gardez moins de trois flashs par seconde (WCAG 2.3.1) : une boucle de moins de 0,34 s, ou beaucoup de cycles sur une plage courte, qui alterne clair et sombre sur une grande surface les dépasse.

Performances

  • Rien ne tourne pour rien : une boucle hors écran ou en pause s'endort et ne demande plus d'image ; un pilote au défilement ne travaille que quand la progression change, et ne mesure rien sous mouvement réduit ; la fonction n'est jamais appelée deux fois pour la même phase, et rien n'est alloué à chaque image.
  • Aucune lecture du DOM de sa part pendant la boucle : la mesure du défilement est celle de ViewProgress, groupée avec celles des autres éléments.
  • Poids : 1,4 ko compressé, sans dépendance ; 5,4 ko avec FrameLoop et ViewProgress, partagés par les autres éléments de la page.

Notes et limites

  • Un pilote par élément. Plusieurs animations qui doivent rester synchrones lisent la même phase : un seul pilote et une seule fonction.
  • Le dt de FrameLoop est borné à 0,05 s : après une image très longue, la boucle reprend au lieu de sauter.
  • scrollSubject est résolu à la création et à chaque changement d'option : si vous déplacez l'élément dans le DOM, recréez le pilote.
  • Ne déplacez pas un dossier d'O-UI seul : PhaseDriver importe ../frame-loop/ et ../view-progress/.
  • Navigateurs : tous les navigateurs actuels (IntersectionObserver, matchMedia).