Aller au contenu
O-UI

Recherche

Trouvez un composant ou une page de documentation.

ScrollTint

BêtaEffet

La couleur d'un élément suit sa place à l'écran : un dégradé couvre la zone visible, et chaque élément en prend la teinte au défilement.

Frameworks
ReactVueVanilla
Technologies
CSS
Déclenché par
Défilement
Mis à jour le
Paramètres

ScrollTint donne à un élément la couleur de l'endroit où il se trouve à l'écran. Un dégradé couvre la zone visible, du haut vers le bas, et chaque élément teinté prend la couleur du dégradé à sa position : quand un bouton monte vers le haut de l'écran, il passe du rose au bleu. Deux éléments au même endroit ont exactement la même couleur, ce qui dessine un dégradé continu derrière toute l'interface, comme les bulles d'une messagerie.

Utilisez-le pour lier une couleur à la position : bulles d'un fil de discussion, boutons ou liens le long d'une page, accents d'une liste. Pour une animation qui progresse au défilement (une apparition, une barre de lecture), préférez ViewProgress.

Installation

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

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

ScrollTint s'appuie sur ViewProgress et FrameLoop, installés avec lui.

Utilisation

import { ScrollTint } from "@/components/o-ui/scroll-tint/scroll-tint";

export function Bubble({ children }: { children: string }) {
  return (
    <ScrollTint className="rounded-2xl bg-blue-600 px-4 py-2 text-white">{children}</ScrollTint>
  );
}

Dans Vue, le dossier installé expose un index : importez ScrollTint et useScrollTint depuis @/components/o-ui/scroll-tint.

  1. Choisissez la couleur du texte avec la palette. ScrollTint ne touche pas au texte de l'élément (sauf avec property="text"). La palette par défaut est faite pour un texte blanc, d'où le text-white de l'exemple : le contraste reste d'au moins 5,1:1 sur tout le dégradé. Le code copié du playground reprend ces classes du rendu « Fond » : avec property="text" ou "border", adaptez-les (exemple « Sommaire et cartes »).
  2. Gardez une couleur de repli proche de la palette (bg-blue-600 ici). Avant le chargement du script, sans JavaScript ou en contraste élevé, l'élément garde ses propres styles.
  3. Le dégradé couvre la zone qui défile. C'est le premier ancêtre en overflow: scroll, ou en overflow: auto dont le contenu déborde déjà, sinon la fenêtre. Un ancêtre en overflow: hidden, comme une carte arrondie, est ignoré. L'option source désigne une autre zone.
  4. Deux rendus. mode="solid" (par défaut) donne une seule couleur, celle du dégradé au centre de l'élément : pour les boutons, les liens, les badges. mode="window" montre la tranche du dégradé qui se trouve derrière l'élément, fixe à l'écran : pour les grandes surfaces, comme des bulles ou des cartes. Il ne s'applique qu'au fond ; avec une autre property, le rendu est solid.
  5. Pour un bouton, un lien ou un paragraphe, utilisez le hook useScrollTint sur votre propre élément (voir « Utiliser sans enveloppe ») : la prop as du composant ne propose que des conteneurs.

Exemples

Fil de discussion

Les bulles envoyées utilisent mode="window" : le dégradé reste fixe pendant qu'elles glissent dessus, et la conversation forme une seule bande de couleur. La zone de défilement est le fil lui-même, trouvée sans réglage.

Boutons le long d'une page

Bleu clair en haut de l'écran, jaune en bas : chaque bouton change de couleur en traversant l'écran. Un rose au milieu évite le gris terne du mélange direct du bleu et du jaune. Le bouton est teinté par le hook useScrollTint, sans élément en plus. Le texte sombre garde un contraste d'au moins 10,8:1 sur tout le dégradé.

Sommaire et cartes

property="text" teinte le texte des liens du sommaire. Sur les cartes, property="variable" ne pose que la variable --scroll-tint, que la page utilise pour une barre d'accent. La palette passe par des variables CSS, plus foncées en clair et plus claires en sombre : le texte reste lisible dans les deux thèmes (au moins 5,9:1).

Carrousel

Avec axis="x", le dégradé va du bord gauche de la zone à son bord droit (de droite à gauche dans une page en rtl). Le rendu window étale un seul dégradé sur toutes les cartes.

Effets combinés

ScrollTint, GlowBorder et Magnetic sur un même bouton, chacun par son hook. GlowBorder reprend le fond du bouton, donc sa teinte. Magnetic déplace le bouton dans une zone fixe : la couleur suit la position du bouton dans la page, pas son petit déplacement. Pour reprendre ce code, installez aussi GlowBorder et Magnetic.

Personnalisation

  • La palette. De 2 à 6 couleurs CSS, du haut de la zone vers le bas, réparties à intervalles égaux. Les variables du thème fonctionnent (colors={["var(--primary)", "var(--chart-2)"]}) : le navigateur les résout sur l'élément, et la teinte suit le passage du clair au sombre.
  • Le mélange. Entre deux couleurs voisines, le mélange se fait en OKLab, l'espace par défaut de CSS. interpolation="oklch" donne des tons intermédiaires plus vifs, mais fait le tour du cercle chromatique : du bleu au jaune, il passe par le vert. Entre deux couleurs opposées, comme le bleu et le jaune, le mélange passe par un gris terne : ajoutez plutôt une couleur au milieu, comme dans l'exemple « Boutons le long d'une page ».
  • La variable --scroll-tint. En rendu solid, elle porte la couleur courante, quelle que soit property, et les enfants en héritent : une icône, une ombre ou une barre d'accent prennent la même couleur. Donnez-lui une valeur de repli, pour le premier rendu, sans JavaScript et en contraste élevé, où elle n'existe pas : var(--scroll-tint, var(--primary)) en CSS, bg-(--scroll-tint,var(--color-primary)) ou fill-(--scroll-tint,currentColor) avec Tailwind v4. En rendu window, la variable n'est pas posée : utilisez solid pour la partager avec les enfants.
  • Une seule teinte solid par élément. Deux ScrollTint solid sur le même élément partagent la même variable : la dernière palette posée vaut pour les deux. Pour teinter deux propriétés, utilisez property="variable" et vos classes (text-(--scroll-tint) border-(--scroll-tint)).
<ScrollTint property="variable" className="relative rounded-xl border bg-card p-5">
  <span
    aria-hidden="true"
    className="absolute inset-y-4 left-0 w-1 rounded-full bg-(--scroll-tint,var(--color-primary))"
  />
  Card content
</ScrollTint>

Intégration

Next.js et Nuxt

ScrollTint est un composant client, utilisable dans un Server Component. Le rendu serveur produit l'élément avec ses propres styles ; la teinte s'applique au chargement du script, dès la première image. En Nuxt, aucun <ClientOnly> n'est nécessaire.

Utiliser sans enveloppe

Pour teinter un élément de votre propre composant, sans l'élément racine que pose ScrollTint, utilisez le hook useScrollTint en React, ou le composable du même nom en Vue. Les options sont les mêmes.

"use client";

import { type ReactNode, useRef } from "react";
import { useScrollTint } from "@/components/o-ui/scroll-tint/scroll-tint";

export function Action({ children }: { children: ReactNode }) {
  const buttonRef = useRef<HTMLButtonElement>(null);
  useScrollTint(buttonRef, { colors: ["#93c5fd", "#f9a8d4", "#fde047"] });
  return (
    <button
      ref={buttonRef}
      type="button"
      className="rounded-full bg-sky-300 px-5 py-2.5 text-neutral-950"
    >
      {children}
    </button>
  );
}
  • Le hook et le composable ne renvoient rien. Une option retirée revient à sa valeur par défaut.
  • L'élément : une ref ou un élément en React ; un élément, une ref de template ou un getter en Vue.
  • Les options : un objet en React. En Vue, un objet, une ref ou un getter ; pour des options qui changent, passez un getter (() => ({ colors: props.colors })), car un objet littéral n'est lu qu'une fois.
  • source accepte aussi la zone de défilement sous forme de ref (React) ou de ref de template et de getter (Vue).
  • Les styles posés. Le rendu solid pose en ligne --scroll-tint et, sauf avec property="variable", la propriété teintée. Le rendu window ajoute un calque en dernier enfant, et pose isolation: isolate et, si besoin, position: relative. Les styles d'origine reviennent au démontage. Si votre framework réécrit le contenu de l'élément, le calque revient à l'image suivante.
  • Les effets du calque window. Un enfant n'est plus le dernier (:last-child, last:), et un enfant en position: absolute se place désormais par rapport à l'élément. Un élément sans enfants possibles (<input>, <img>) ne peut pas recevoir de calque : teintez son conteneur, ou utilisez solid.

Choisir la zone de défilement

Par défaut, la zone est trouvée seule. Passez source="viewport" pour un dégradé sur toute la fenêtre, même dans une zone qui défile, ou un élément ancêtre pour une zone précise. Un élément qui n'est pas un ancêtre est ignoré.

const threadRef = useRef<HTMLDivElement>(null);
// …
<div ref={threadRef} className="overflow-y-auto">
  <ScrollTint source={threadRef}>…</ScrollTint>
</div>;

Une zone en overflow: auto qui ne déborde pas encore au premier rendu (un fil dont les messages arrivent ensuite) n'est pas trouvée seule : la couleur se corrige à son premier défilement. Pour l'avoir dès le départ, passez-la à source, ou donnez-lui overflow-y: scroll.

Un texte automatique noir ou blanc

Pour une palette qui va du sombre au clair, contrast-color() choisit un texte noir ou blanc selon la teinte, dans les navigateurs qui la connaissent. Gardez une couleur sûre pour les autres, et une valeur de repli pour la variable :

.auto-text {
  color: white;
}
@supports (color: contrast-color(red)) {
  .auto-text {
    color: contrast-color(var(--scroll-tint, #1e3a8a));
  }
}
<ScrollTint colors={["#1e3a8a", "#fde68a"]} className="auto-text">
  …
</ScrollTint>

Le choix se fait par le calcul de luminance de la WCAG 2 : sur un bleu moyen, il peut retenir le noir alors que le blanc contraste mieux. Vérifiez les contrastes de votre palette ; le texte bascule à un endroit fixe de l'écran, sans clignoter.

API

PropTypeDéfautDescription
colorsreadonly string[]["#2563eb", "#7c3aed", "#be185d"]De 2 à 6 couleurs CSS, variables comprises, du début de la zone visible (en haut, ou à gauche) à sa fin. Une liste de moins de deux couleurs valides garde la palette courante ; au-delà de six, la liste est coupée.
modeScrollTintMode"solid"`"solid"` : une couleur, celle du dégradé au centre de l'élément. `"window"` : la tranche du dégradé derrière l'élément, fixe à l'écran. `window` ne s'applique qu'au fond : avec une autre `property`, le rendu est `solid`.
propertyScrollTintProperty"background"Ce qui est teinté : `"background"` (le fond), `"text"` (la couleur du texte), `"border"` (la couleur de la bordure, dont l'épaisseur et le style restent les vôtres) ou `"variable"` (la variable `--scroll-tint` seule).
interpolationScrollTintInterpolation"oklab"Espace du mélange entre deux couleurs voisines : `"oklab"` (l'espace par défaut de CSS) ou `"oklch"`, plus vif, qui fait le tour du cercle chromatique.
axis"x" | "y""y"Défilement vertical (`"y"`) ou horizontal (`"x"`), pour un carrousel.
sourceScrollTintSource"auto"La zone que le dégradé couvre : `"auto"` (le premier ancêtre qui défile vraiment, sinon la fenêtre), `"viewport"`, ou un élément ancêtre. Un autre élément garde la valeur courante.
disabledbooleanfalseRetire la teinte et rend à l'élément ses styles d'origine, sans démonter le composant.

Une valeur invalide (mauvais type, valeur inconnue, couleur que le navigateur refuse, mot-clé comme inherit) garde la valeur courante, ou la valeur par défaut à la création. Une variable (var(--x)) est acceptée telle quelle : si elle n'est pas définie sur l'élément, la teinte devient invalide et l'élément garde ses propres styles. colors et interpolation changent en direct ; les autres options s'appliquent à l'image suivante, sans clignotement.

Les adaptateurs transmettent les attributs natifs à la racine, ainsi que className en React et class en Vue. La prop as choisit l'élément racine (div par défaut, span, li, article, section ou aside). La racine est accessible par ref en React, et par el (defineExpose) en Vue. Le hook et le composable useScrollTint prennent les mêmes options (voir « Utiliser sans enveloppe »).

Accessibilité

Ce que fait ScrollTint

  • Il ne change que des couleurs : le rôle, le nom, la navigation au clavier et le comportement de l'élément restent les vôtres.
  • Le calque du rendu window est décoratif (aria-hidden="true") et ne capte pas les clics.
  • En contraste élevé (forced-colors), la teinte s'efface et l'élément reprend les couleurs du système ; elle revient quand le mode se termine.

Ce que vous devez faire

  • Garder le texte lisible sur toute la palette : au moins 4,5:1 pour un texte normal, 3:1 pour un grand texte. La palette par défaut le garantit avec un texte blanc (5,1:1 au pire), pas avec un texte sombre (3,3:1 au pire avec text-neutral-950).
  • Avec property="text", choisir une palette adaptée au fond, en clair comme en sombre : des variables CSS par thème, comme dans l'exemple « Sommaire et cartes ».
  • Signaler les états (focus, survol, désactivé, erreur) autrement que par la propriété teintée. La teinte est posée en style en ligne : elle l'emporte sur une classe d'état qui changerait la même propriété (focus-visible:bg-…). Utilisez un anneau extérieur, un outline ou l'opacité ; ou bien property="variable" avec vos classes (bg-(--scroll-tint) focus-visible:bg-…), où vos classes d'état s'appliquent normalement.
  • En rendu window, le calque recouvre les ombres internes de l'élément, dont un anneau de focus ring-inset. Sur un élément focalisable, préférez un outline (focus-visible:outline-2 focus-visible:-outline-offset-2) ou un anneau extérieur.
  • Ne pas porter d'information par la seule couleur : la teinte décrit une position, pas un état.

Mouvement réduit. Une couleur n'est pas un mouvement, et ScrollTint n'anime rien de lui-même : il ne suit que le défilement de la personne. En rendu window, le dégradé reste immobile à l'écran. Sans ViewTimeline (Firefox), le calque est recalé à chaque image et peut sembler glisser d'une image pendant le défilement : avec le mouvement réduit, window y devient solid.

Pause. Rien à mettre en pause (WCAG 2.2.2) : sans défilement, rien ne bouge.

Performances

  • Le cœur et ses fichiers annexes pèsent environ 2,6 kB minifiés et compressés (5,5 kB minifiés), sans dépendance. Avec ViewProgress et FrameLoop, l'ensemble pèse 6,6 kB.
  • Dans Chrome, Edge et Safari 26, le navigateur suit le défilement seul (une ViewTimeline) : ScrollTint ne mesure rien et ne demande aucune image pendant le défilement. Ailleurs, ViewProgress mesure les éléments proches de l'écran, une fois par image et tous à la fois.
  • mode="window" ne déplace qu'un calque composité : ni peinture ni mise en page pendant le défilement. mode="solid" repeint l'élément, car sa couleur change, sans mise en page.
  • Mesuré : 300 bulles dans une zone qui défile tiennent 60 images par seconde, en solid comme en window, avec ou sans ViewTimeline. Un élément hors de l'écran ne coûte rien.
  • En window, chaque élément visible a son propre calque composité, de la longueur de la zone visible. Gardez ce rendu pour des surfaces en nombre raisonnable à l'écran (des bulles, des cartes) ; pour de nombreux petits éléments, préférez solid.

Problèmes fréquents

  • La couleur ne change pas au défilement. L'élément ne bouge pas dans sa zone : il est dans un en-tête fixe ou collant, ou la zone désignée par source ne le fait pas défiler. C'est voulu : la couleur décrit une place à l'écran.
  • La couleur suit la fenêtre, pas le conteneur qui défile. Deux causes : source="viewport" est passé (retirez-le), ou le conteneur ne déborde pas encore au premier rendu. Dans ce second cas, la couleur se corrige à son premier défilement ; pour l'éviter, passez le conteneur à source ou donnez-lui overflow-y: scroll.
  • En window, le dégradé s'arrête au bord de la bordure. Le calque couvre l'intérieur de la bordure : la bordure garde sa propre couleur. Pour la teinter aussi, ajoutez un second ScrollTint avec property="border" (un seul ScrollTint solid par élément).
  • window ne fait rien sur le texte, ou la variable manque. window ne s'applique qu'au fond, et ne pose pas --scroll-tint : avec une autre property, ou pour partager la couleur avec les enfants, le rendu est solid.
  • La couleur traîne derrière la position pendant le défilement. Une transition sur la propriété teintée (transition-colors d'un bouton) la retarde. Retirez la transition de cette propriété.

Notes et limites

  • Navigateurs : color-mix(), les dégradés en OKLab et CSS.registerProperty (Chrome 111, Safari 16.4, Firefox 128 et versions plus récentes). Sans eux, l'élément garde ses propres styles.
  • La position qui compte est celle de la mise en page, comme en CSS : une transformation de l'élément (un petit déplacement, une mise à l'échelle) ne change pas sa couleur. Quand ViewProgress mesure la position (Firefox notamment), une transformation d'un ancêtre la décale, elle.
  • Le dégradé couvre la zone visible : un élément plus grand que la zone n'en montre, en window, que la partie visible.
  • Après un saut de défilement très grand en une image (touche Fin, lien d'ancre), hors de Chrome, Edge et Safari, un élément peut garder sa couleur précédente pendant une image.