Aller au contenu
O-UI

Recherche

Trouvez un composant ou une page de documentation.

FallingText

BêtaComposant

Fait tomber les lettres d'un titre jusqu'à leur place : elles rebondissent un peu, puis se posent.

Frameworks
ReactVueVanilla
Technologies
GSAP
Déclenché par
Apparition à l'écranPiloté par l'application
Mis à jour le
Paramètres

FallingText fait tomber les lettres d'un titre jusqu'à leur place : elles touchent leur ligne de base, rebondissent un peu, s'écrasent brièvement, puis se posent exactement. L'entrée se joue une fois, sauf si vous la rejouez, et dure moins de cinq secondes. Utilisez-le pour le titre principal d'une page (voir l'encadré sur le LCP), quelques titres de section ou un message court (confirmation, page d'erreur). Évitez-le sur un paragraphe, un titre long ou un titre répété dans une liste.

Installation

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

pnpm dlx shadcn@latest add https://o-ui.hd987.dev/r/falling-text.json
  • GSAP et son plugin SplitText, qui découpe le texte, sont installés comme dépendance (gsap 3.13 ou plus récent). Voir la licence dans Notes et limites.
  • Tailwind CSS 4.1 ou plus récent. Les adaptateurs masquent le titre jusqu'à l'hydratation. Sans JavaScript et à l'impression, il reste visible grâce aux variantes noscript: et print:, qui demandent Tailwind 4.1. Sans Tailwind 4.1, ajoutez data-falling-text à FallingText (l'attribut est transmis au titre) et reprenez le CSS de l'onglet Vanilla ci-dessous.

Utilisation

Écrivez le texte du titre dans FallingText. as choisit la balise, h2 par défaut.

import { FallingText } from "@/components/o-ui/falling-text/falling-text";

export function Hero() {
  return (
    <FallingText as="h1" className="text-6xl font-bold tracking-tight">
      Built to fall into place.
    </FallingText>
  );
}

FallingText est un composant client : cet extrait fonctionne tel quel dans un Server Component de Next.js. Un fichier qui lui passe onStart, onComplete, une ref ou actionsRef doit commencer par "use client".

Quelques règles font la différence :

  1. Un texte court, sans lien ni bouton. Un titre qui contient un élément interactif s'affiche tel quel, sans animation. Au-delà d'une trentaine de lettres, préférez split="words" : les départs étant étalés sur 1,5 s au plus, les lettres partiraient presque ensemble.
  2. Laissez la chute visible. Avec from="screen", les lettres partent du haut de l'écran : un ancêtre en overflow: hidden les couperait. Dans une carte ou un conteneur qui coupe, donnez une hauteur de chute en em du titre, par exemple from={2}.
  3. Un titre est un bloc. Ne le passez pas en inline (par exemple avec className="inline" pour l'insérer dans une phrase) : le texte pourrait changer de lignes à l'arrivée.

Quand la chute se joue

  • trigger="visible" (par défaut) : une seule fois, quand un quart du titre est à l'écran. Un titre caché (dans une modale fermée, par exemple) joue à sa première apparition.
  • trigger="load" : dès le montage. Un titre hors de l'écran à ce moment arrive directement à sa place.
  • trigger="manual" : seulement quand vous appelez play() ou replay().

play() lance la première lecture et ne fait plus rien ensuite. replay() remet les lettres en haut et rejoue, quel que soit le déclencheur. Les deux respectent disabled et le mouvement réduit.

Avec les valeurs par défaut, l'entrée dure environ 2,5 s pour une douzaine de lettres et un peu plus de 3 s pour une vingtaine, sur ordinateur comme sur mobile. Elle ne dépasse jamais 5 s, delay mis à part : au besoin, la chute est accélérée et les rebonds amortis. Une animation aussi courte ne demande aucun contrôle de pause.

Exemples

Titre d'accueil

Le paragraphe apparaît quand le titre s'est posé, grâce à onComplete en React ou à @complete en Vue. Sans JavaScript ou avec le mouvement réduit, il est visible d'emblée : la révélation ne cache jamais le contenu. onComplete n'est pas appelé si le titre ne s'anime pas (disabled, contenu interactif) : le paragraphe s'affiche alors grâce à data-state (voir Enchaîner après la chute).

Titres de sections

Faites défiler la page : chaque titre tombe quand il entre à l'écran. Pour des titres secondaires, une chute courte (from={2}, en em) suffit ; la chute depuis le haut de l'écran reste réservée au titre principal.

  • Le premier fait tomber des mots entiers (split="words"), plus calme sur un titre long.
  • Le deuxième part du milieu (order="center"), le troisième dans un ordre aléatoire (order="random").
  • Chaque titre a sa seed : deux titres ne tombent jamais de la même façon.

Message de confirmation

Le message est monté avec le nouvel état : trigger="load" le joue aussitôt, sans attendre qu'un quart du titre soit à l'écran. La carte coupe ce qui dépasse (overflow: hidden) : le titre tombe de 2 em au-dessus de sa place (from={2}), à l'intérieur. Le focus passe à la confirmation, puis revient au champ quand on écrit un autre message.

Page introuvable

Un mot très court supporte une chute plus vive : plus de rebond (bounce={0.45}), d'écrasement (squash={0.6}) et d'inclinaison (rotation={24}), et des départs plus espacés (stagger={0.12}).

Personnalisation

  • Lourde : aucune prop. Une chute franche et un petit rebond.
  • Rebondissante : duration={0.9}, bounce={0.55} et squash={0.7}, pour une marque plus joueuse.
  • Sobre : bounce={0.1}, rotation={0}, drift={0} et randomness={0.2}, pour une page plus retenue.

Le bouton « Replay » de cet exemple appelle replay() sur les trois titres (voir Rejouer la chute).

Les options se répartissent en trois groupes :

  • Le rythme. duration règle la gravité : le temps d'une chute sur la moitié de l'écran, et non la durée de l'entrée. La gravité suit la hauteur de l'écran, si bien que l'entrée garde la même allure sur mobile. stagger et order règlent les départs, delay l'attente avant le premier, par exemple pour laisser d'abord apparaître un surtitre.
  • Le mouvement de chaque lettre. bounce règle les rebonds, squash l'écrasement à l'impact, rotation et drift l'inclinaison et l'écart horizontal au départ.
  • La variété. randomness donne à chaque lettre son propre départ, sa hauteur, son rebond et son inclinaison. Les variations dépendent de seed : un titre tombe toujours de la même façon, et des seed différentes rendent deux titres voisins différents.

Intégration

Next.js et Nuxt

Le rendu serveur produit le titre avec son vrai texte. Les adaptateurs le masquent jusqu'à l'hydratation, pour éviter un flash, puis il tombe.

  • Next.js : FallingText est un composant client, utilisable dans un Server Component. Un fichier qui lui passe onStart, onComplete, une ref ou actionsRef doit commencer par "use client".
  • Nuxt : aucun <ClientOnly> n'est nécessaire. Importez FallingText explicitement, comme dans les exemples : l'import automatique de Nuxt préfixe le nom d'un composant par son dossier.

Rejouer la chute

En React, la ref reste l'élément du titre ; actionsRef reçoit { play, replay }.

"use client";

import { useRef } from "react";
import { FallingText, type FallingTextActions } from "@/components/o-ui/falling-text/falling-text";

export function Title() {
  const actionsRef = useRef<FallingTextActions | null>(null);
  return (
    <>
      <FallingText actionsRef={actionsRef}>Built to fall into place.</FallingText>
      <button type="button" onClick={() => actionsRef.current?.replay()}>
        Replay
      </button>
    </>
  );
}

Pour rejouer à chaque ouverture d'une modale, appelez replay() à l'ouverture : trigger="visible" ne joue qu'une fois.

Enchaîner après la chute

onStart et onComplete (en Vue, @start et @complete) signalent le départ et l'arrivée de chaque lecture, replay() comprise. En CSS, sans callback, appuyez-vous sur data-state, posé sur le titre :

ValeurSignification
idletrigger="manual", en attente de play()
waitingen attente du déclencheur (ou des polices)
playingla chute est en cours, delay compris
doneles lettres sont posées
reducedle titre apparaît ou est apparu en fondu (mouvement réduit)
disabledle titre s'affiche tel quel (disabled ou contenu interactif)

Avec Tailwind, marquez le titre de peer et révélez l'élément suivant selon son état ; noscript: le garde visible sans JavaScript. Le titre d'accueil combine ce principe, pour l'état disabled, avec onComplete :

<>
  <FallingText className="peer">Built to fall into place.</FallingText>
  <p className="opacity-0 transition-opacity peer-data-[state=disabled]:opacity-100 peer-data-[state=done]:opacity-100 peer-data-[state=reduced]:opacity-100 noscript:opacity-100">
    The rest of the hero.
  </p>
</>

Un texte qui change

Au repos, le titre se met simplement à jour, sans rejouer. Si le texte change pendant la chute, la chute s'arrête et le nouveau texte s'affiche aussitôt, sans animation. Pour faire tomber le nouveau texte, appelez replay() après le changement.

API

PropTypeDéfautDescription
durationnumber1.2Rythme de la chute : le temps de tomber sur la moitié de la hauteur de l'écran, en secondes, de 0,2 à 3. Il fixe la gravité, pas la durée totale : plus court, la chute est plus vive.
staggernumber0.05Écart entre deux départs, en secondes, de 0 à 0,5. L'étalement total est plafonné à 1,5 s : sur un titre long, l'écart réel est plus court.
orderFallingTextOrder"start"Ordre des départs : `"start"` (du début), `"end"` (de la fin), `"center"` (du milieu vers les bords), `"edges"` (des bords vers le milieu) ou `"random"` (selon `seed`).
delaynumber0Attente avant le premier départ, en secondes, de 0 à 10. Le titre reste transparent pendant ce délai.
bouncenumber0.25Hauteur des rebonds, de 0 à 0,9 : la part de la vitesse conservée à chaque impact. À 0, les lettres se posent sans rebondir ; vers 0,5, elles rebondissent franchement.
squashnumber0.35Écrasement à l'impact et étirement pendant la chute, de 0 à 1. À 0, les lettres restent rigides.
rotationnumber12Inclinaison maximale au départ, en degrés, de 0 à 90. Les lettres se redressent en tombant ; à 0, elles tombent droites.
driftnumber0.2Écart horizontal maximal au départ, en em, de 0 à 4. À 0, chaque lettre tombe à la verticale de sa place.
randomnessnumber0.5Variations propres à chaque lettre (départ, hauteur, rebond, inclinaison, écart), de 0 à 1. À 0, la chute est parfaitement régulière.
seednumber1Graine des variations, entier positif ou nul (une fraction est arrondie vers le bas). Une même graine donne toujours la même chute : donnez-en une différente à chaque titre d'une page.
fromnumber | "screen""screen"`"screen"` : les lettres partent du haut de l'écran. Un nombre : la hauteur de chute, en em, de 0,5 à 100, pour un titre secondaire ou dans un conteneur qui coupe ce qui dépasse.
split"chars" | "words""chars"`"chars"` fait tomber les lettres, sans jamais couper un mot en fin de ligne ; `"words"` fait tomber les mots, plus calme sur un titre long et indispensable pour une écriture liée comme l'arabe.
trigger"load" | "manual" | "visible""visible"`"visible"` joue une fois, quand un quart du titre est à l'écran ; `"load"` joue dès le montage ; `"manual"` attend `play()` ou `replay()`.
reducedMotionReducedMotion"user"`"user"` suit le réglage du système, `"always"` remplace toujours la chute par un fondu, `"never"` garde la chute : à réserver au cas où la personne a réactivé les animations dans les réglages de votre site.
disabledbooleanfalseBooléen : affiche le titre tel quel, sans découpage ni animation. `onStart` et `onComplete` ne sont alors pas appelés ; passé à `true` pendant une chute, il la termine aussitôt, et `onComplete` est appelé.
onStart(() => void)undefinedAppelé au départ de chaque lecture, `replay()` comprise (React et vanilla ; événement `start` en Vue).
onComplete(() => void)undefinedAppelé quand toutes les lettres sont posées, à chaque lecture (React et vanilla ; événement `complete` en Vue). Jamais appelé si le titre ne s'anime pas (`disabled`, contenu interactif).

Les nombres hors plage sont ramenés dans la plage. Une valeur invalide garde la valeur courante, ou la valeur par défaut à la création. Le playground se limite aux valeurs utiles, et part toujours du haut de l'écran : essayez from dans votre code. Les options d'animation s'appliquent à la lecture suivante : une chute en cours n'est jamais interrompue. disabled et reducedMotion s'appliquent tout de suite, comme trigger tant que le titre n'a pas joué.

Les adaptateurs ajoutent as, la balise du titre (h1 à h6, p ou div, h2 par défaut), et transmettent les attributs natifs, ainsi que className en React et class en Vue. React ajoute actionsRef ; en Vue, la ref expose { el, play, replay }. Le cœur expose play(), replay(), update() et destroy().

Accessibilité

Ce que fait FallingText

  • Le titre garde son vrai texte et son niveau : les lecteurs d'écran l'annoncent une fois, en entier. Les lettres qui tombent sont une copie décorative (aria-hidden), retirée à l'arrivée.
  • Au repos, le titre est exactement son contenu d'origine : ligatures, sélection et copier-coller fonctionnent normalement.
  • L'entrée dure moins de 5 s, sans clignotement : elle ne demande aucun contrôle de pause.
  • FallingText n'ajoute ni rôle ni tabindex, et ne réagit pas au pointeur.
  • Sans JavaScript et à l'impression, le titre est visible.

Ce que vous devez faire

  • Choisir le bon niveau de titre avec as.
  • Ne pas mettre de lien ni de bouton dans le titre : il s'afficherait sans animation.
  • Si un contenu n'apparaît qu'après la chute, comme le paragraphe du titre d'accueil, le rendre visible d'emblée sans JavaScript, avec le mouvement réduit et quand le titre ne s'anime pas.

Mouvement réduit. La chute est remplacée par un fondu de 0,2 s sur le texte d'origine, sans découpage. delay s'applique toujours avant le fondu. Si le réglage change pendant une chute, les lettres se posent aussitôt. La prop reducedMotion permet de forcer ce comportement ou de l'ignorer.

Contraste élevé. FallingText n'impose aucune couleur : le titre garde les siennes, et le mode forced-colors s'applique normalement.

Performances

  • Le cœur et ses fichiers annexes pèsent environ 6,4 kB minifiés et compressés (14,8 kB minifiés), plus GSAP (environ 28 kB, partagés avec vos autres animations GSAP) et SplitText (environ 3,7 kB).
  • Une seule animation pilote chaque titre, et seules la transformation et l'opacité des lettres changent : aucune mise en page pendant la chute.
  • Les mesures n'ont lieu qu'au départ d'une lecture. Le titre attend au plus 1 s les polices en cours de chargement, pour ne pas mesurer une police de repli.
  • Rien ne tourne hors de l'écran ni au repos : un titre qui sort de l'écran pendant sa chute se pose aussitôt.
  • Quelques titres par page restent légers ; évitez d'en faire tomber des dizaines à la fois, chacun découpant son texte au départ.

Problèmes fréquents

  • Le titre reste invisible. Il attend son déclencheur : avec trigger="manual", appelez play(). En vanilla, vérifiez que le cœur a bien été créé, et que destroy() n'a pas été appelé sur un titre resté dans la page.
  • Le titre ne s'anime pas. Il contient un lien, un bouton ou un élément focalisable, disabled est actif, ou le système demande de réduire les animations (le titre apparaît alors en fondu).
  • La chute est coupée. Un ancêtre a overflow: hidden. Donnez une hauteur de chute en em (from={2}).
  • onComplete n'est jamais appelé. Le titre ne s'anime pas (voir ci-dessus) : affichez d'emblée ce qui en dépend.
  • Deux titres tombent exactement de la même façon. Ils ont la même seed et la même longueur. Donnez une seed différente à chacun.
  • Sans JavaScript, le titre reste caché. Les variantes noscript: demandent Tailwind CSS 4.1 ou plus récent.

Notes et limites

  • GSAP 3.13 ou plus récent est requis pour SplitText v2. GSAP est distribué sous la licence « Standard No-Charge », gratuite y compris pour un usage commercial, mais ce n'est pas une licence open source. Voir gsap.com/standard-license.
  • Sans IntersectionObserver, matchMedia ou MutationObserver, le titre s'affiche tel quel, sans animation.
  • Pour une écriture dont les lettres se lient, comme l'arabe, utilisez split="words".
  • Quand un mot ne tient pas sur une ligne (un texte sans espaces, comme en chinois ou en japonais, ou un mot plus long que la ligne), le titre apparaît en fondu sur place.
  • Pendant la chute, l'encre qui dépasse de la boîte du texte (italique, lettres ornées) peut être coupée sur les bords du titre ; jamais au repos.
  • Le titre reste masqué tant que le script ne s'est pas exécuté. Si JavaScript est actif mais que le script ne se charge pas (erreur réseau), il reste donc invisible ; sans JavaScript, il s'affiche normalement.