CSS scroll-state()

Comme les requêtes de conteneur, mais pour les requêtes bloquées, figées et débordantes.

Publié le 15 janvier 2025

Chrome 133 s'appuie sur les requêtes de conteneur en introduisant des requêtes de conteneur d'état de défilement. L'état géré par le navigateur pour le positionnement persistant, les points d'ancrage de défilement et les éléments à faire défiler peut désormais être interrogé et adapté à partir de CSS.

Présentation

Avant les requêtes d'état de défilement, vous deviez utiliser JavaScript pour déterminer si un élément était bloqué, aligné ou à faire défiler. Il existe désormais une méthode plus performante sur le canal de normes pour connaître ces informations et s'adapter en conséquence. Une nouvelle méthode de déclenchement d'animations est également disponible, qui permet de déclencher des animations à partir du CSS.

Voici un aperçu des requêtes d'état disponibles à partir de Chrome 133:

État bloqué:
Le style de déclenchement change lorsqu'un élément est collé à un bord.
État verrouillé:
Déclenche des modifications de style lorsqu'un élément est aligné sur une axe.
État de défilement:
Le style du déclencheur change lorsqu'un élément déborde.

La bonne nouvelle est que tout ce que vous avez appris sur les requêtes de conteneur vous aidera à travailler avec les requêtes d'état de défilement.

Le territoire entre les animations déclenchées par le défilement et les requêtes de conteneur d'état de défilement est également inexploré. Nous devons tester le timing et le contexte pour déterminer si une animation déclenchée par le défilement ou une animation d'état de défilement déclenchée par le défilement est la meilleure. La vidéo et la démonstration suivantes illustrent la situation : une animation déclenchée par un élément persistant par rapport à une animation déclenchée par le défilement.

(à gauche) Animation déclenchée par scroll-state(), (à droite) animation basée sur le défilement
https://codepen.io/web-dot-dev/pen/emOrBaV

Première requête d'état de défilement

La première étape consiste à définir le conteneur, en utilisant une nouvelle valeur pour la propriété container-type. Comme pour une requête de conteneur, l'élément que vous souhaitez interroger est celui auquel vous attribuez container-type et éventuellement container-name. Avec les requêtes d'état de défilement, vous indiquez l'élément qui s'ancre, est bloqué ou présente un débordement container-type: scroll-state.

.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;
}

La deuxième étape consiste à sélectionner l'enfant de ce conteneur qui répondra à l'état. Comme pour les requêtes de conteneur, il ne peut pas s'agir du même élément qui comporte le container-type.

.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;

  > nav {
    @container scroll-state(stuck: top) {
      background: Highlight;
      color: HighlightText;
    }
  }
}

La troisième étape consiste à l'essayer. L'exemple CSS suivant applique un style rouge à l'arrière-plan lorsque l'élément .stuck-top se fixe en haut à 0. Avec quelques lignes supplémentaires dans le CSS que nous aurions déjà écrit et un élément contenant supplémentaire qui met en proxy l'état du navigateur, nos composants sont beaucoup plus intelligents concernant leur environnement.

https://codepen.io/web-dot-dev/pen/ByBxpwR

Amélioration progressive

La règle d'instruction et l'imbrication @supports vous permettent d'ajouter une amélioration progressive ou une utilisation conditionnelle des fonctionnalités en seulement quelques lignes de code supplémentaires:

.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;

  @supports (container-type: scroll-state) {
    > nav {
      @container scroll-state(stuck: top) {
        background: Highlight;
        color: HighlightText;
      }
    }
  }
}

N'oubliez pas non plus d'utiliser @media (prefers-reduced-motion: no-preference) {} autour de votre mouvement si vous animez des éléments autour de la page à l'aide de requêtes d'état de défilement.

Cas d'utilisation

Coincé

Peut-être que cette section devrait s'intituler "Situations délicates" ? Voici une petite collection de cas d'utilisation de l'état persistant, ainsi qu'une section bonus d'idées à développer.

@container scroll-state(stuck: top) {}
@container scroll-state(stuck: bottom) {}

Liste complète de la syntaxe

Ajouter une ombre en cas de blocage

L'un des cas d'utilisation les plus courants d'une requête bloquée concerne les barres de navigation qui souhaitent ajouter box-shadow lorsqu'elles sont bloquées, afin qu'elles puissent sembler flotter au-dessus du contenu qu'elles superposent.

https://codepen.io/web-dot-dev/pen/GgKdryj
.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;

  > nav {
    transition: box-shadow .3s ease;

    @container scroll-state(stuck: top) {
      box-shadow: var(--shadow-5);
    }
  }
}

Activer l'en-tête bloqué actuel

Un autre scénario courant de retour d'interface utilisateur persistant consiste à mettre en surbrillance l'élément actuellement bloqué. Dans une liste de groupes alphabétiques, cela peut être très utile et améliorer l'expérience.

https://codepen.io/web-dot-dev/pen/pvzVRaK
.sticky-slide {
  dt {
    container-type: scroll-state;
    position: sticky;
    inset-block-start: 0;
    inset-inline: 0;

    > header {
      transition: 
        background .3s ease,
        box-shadow .5s ease;

      @container scroll-state(stuck: top) {
        background: hsl(265 100% 27%);
        box-shadow: 0 5px 5px #0003;
      }
    }
  }
}

Voici une autre variante, dans laquelle les en-têtes se trouvent à côté des éléments de liste. Les possibilités sont nombreuses.

https://codepen.io/web-dot-dev/pen/azoGpGg

Dépassement d'idées

Voici une liste de démonstrations persistantes qui pourraient vous inspirer pour ajouter un peu de piquant à la démonstration ou supprimer leur code JavaScript, avec des requêtes d'état de défilement. Je vous suggère d'essayer d'en créer un qui vous plaît. Cela vous aidera à retenir la syntaxe et les idées 😏.

Intégré

Avec les requêtes d'état ancré, nous pouvons décharger une partie de la responsabilité de JavaScript et des événements d'ancrage, et transférer la gestion vers le CSS.

@container scroll-state(snapped: x) {}
@container scroll-state(snapped: y) {}
@container scroll-state(snapped: inline) {}
@container scroll-state(snapped: block) {}

Liste complète de la syntaxe

Petit rappel, au cas où vous auriez ignoré la section Première requête d'ancrage, le conteneur d'une requête d'ancrage est l'élément contenant scroll-snap-align, et l'élément pouvant s'adapter doit être un enfant de cet élément. Pour ce faire, vous avez besoin de trois éléments:

a scroll container with `scroll-snap-type`
⤷ a snap target with both `scroll-snap-align` and `container-type: scroll-state`
    ⤷ a child of the snap target that can query the container for snap state

Mettre en valeur l'élément épinglé visuellement

Il est très courant, avec un défilement centré, de mettre en avant l'élément centré. Dans cet exemple de témoignages, le mot clé not est utilisé pour que tous les témoignages non épinglés aient une faible opacité, tandis que l'épinglé reste dans son état de présentation naturel.

https://codepen.io/web-dot-dev/pen/NPKMdBX
.demo {
  overflow: auto hidden;
  scroll-snap-type: x mandatory;

  > article {
    container-type: scroll-state;
    scroll-snap-align: center;

    @supports (container-type: scroll-state) {
      > * {
        transition: opacity .5s ease;

        @container not scroll-state(snapped: x) {
          opacity: .25;
        }
      }
    }
  }
}

Afficher la légende de l'élément épinglé

Il s'agit d'un bon exemple de la façon dont les requêtes d'état de défilement permettent d'activer l'animation déclenchée par le défilement. Il s'agit également d'un bon exemple de cas où le respect de la réduction du mouvement est utile dans le CSS.

https://codepen.io/web-dot-dev/pen/XJrqpBG
.demo {
  overflow-x: auto;
  scroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  > .card {
    container-type: scroll-state;
    scroll-snap-align: center;

    @supports (container-type: scroll-state) {
      @media (prefers-reduced-motion: no-preference) {
        figcaption {
          transform: translateY(100%);

          @container scroll-state(snapped: x) {
            transform: translateY(0);
          }
        }
      }
    }
  }
}

Animer des éléments de diapositive

Il est très courant d'animer des éléments d'un diaporama ou d'une présentation lors d'une conférence. Il était auparavant assez pénible d'écrire un observateur d'intersection pour cela, qui ne faisait que définir une classe sur la diapositive. Nous n'avons plus besoin de JavaScript.

https://codepen.io/web-dot-dev/pen/dPbeNqY
html {
  scroll-snap-type: y mandatory;
}

section {
  container-type: scroll-state;
  scroll-snap-align: start;
  scroll-snap-stop: always;

  @supports (container-type: scroll-state) {
    @media (prefers-reduced-motion: no-preference) {
      > h1 {
        transition: opacity .5s ease, transform .5s var(--ease-spring-3);
        transition-delay: .5s;
        opacity: 0;
        transform: scale(1.25);

        @container scroll-state(snapped: block) {
          opacity: 1;
          transform: scale(1);
        }
      }
    }
  }
}

Vous remarquerez peut-être que toutes les requêtes d'état CSS figées se comportent comme scrollsnapchanging, et non comme scrollsnapchange. Vous disposez ainsi du hook le plus précoce possible pour fournir un retour visuel sur l'élément épinglé. Si elle est trop impatiente, envisagez l'événement JavaScript.

Défilante

La requête d'état de défilement est très utile pour afficher les affordances visuelles lorsque la zone de défilement peut effectivement être défilée. Avant les requêtes d'état de défilement, il était difficile de connaître ces informations.

@container scroll-state(scrollable: top) {}
@container scroll-state(scrollable: right) {}
@container scroll-state(scrollable: bottom) {}
@container scroll-state(scrollable: left) {}

Liste complète de la syntaxe

Indique le défilement à l'aide d'ombres

Lea Verou a créé un célèbre astuce CSS qui utilise background-attachment: local pour obtenir un effet semblable, ainsi qu'une autre méthode avec une animation basée sur le défilement. Chaque technique présente des compromis. C'est à nous de déterminer quand et où chacune de ces techniques est la plus adaptée.

L'exemple suivant utilise un seul élément persistant qui s'étend sur le scrollport. L'opacité d'un dégradé en haut et d'un dégradé en bas est animée avec @property lorsque leur requête d'état de défilement contextuel s'applique: @container scroll-state(scrollable: top).

Notez également qu'il s'agit du premier conteneur qui est à la fois un conteneur size et scroll-state.

https://codepen.io/web-dot-dev/pen/OPLZWBj
.scroll-container {
  container-type: scroll-state size;
  overflow: auto;

  &::after {
    content: " ";

    background: var(--_shadow-top), var(--_shadow-bottom);
    transition: 
      --_scroll-shadow-color-1-opacity .5s ease,
      --_scroll-shadow-color-2-opacity .5s ease;

    @container scroll-state(scrollable: top) {
      --_scroll-shadow-color-1-opacity: var(--_shadow-color-opacity, 25%);
    }

    @container scroll-state(scrollable: bottom) {
      --_scroll-shadow-color-2-opacity: var(--_shadow-color-opacity, 25%);
    }
  }
}

Requête de flèche

Parfois, afficher une flèche peut aider les utilisateurs à découvrir qu'une zone est déroulante. Ils ont tendance à indiquer la direction dans laquelle le défilement peut se produire et disparaissent une fois qu'ils ne sont plus nécessaires. Pour ce faire, utilisez le code suivant.

https://codepen.io/web-dot-dev/pen/OPLZWBj
@container scroll-state((scrollable: top) or (not (scrollable: bottom))) {
  translate: 0 calc(100% + 10px);
}

@container scroll-state((scrollable: top) and (not (scrollable: bottom))) {
  translate: 0 calc(100% + 10px);
  rotate: .5turn;
}

Haut de page

Une autre interaction populaire avec l'état de défilement est le bouton pratique "Défiler vers le haut". Le code suivant fait disparaître le bouton de défilement vers le haut lorsqu'il n'y a pas de défilement vers le haut.

Cette solution est un peu inversée, mais elle vous permet de réduire la quantité de CSS. Le bouton est visible par défaut. Vous devez donc lui indiquer de se masquer lorsqu'il n'est plus possible de faire défiler l'écran vers le haut.

https://codepen.io/web-dot-dev/pen/OPLZWBj
@container not scroll-state(scrollable: top) {
  translate: 0 calc(100% + 10px);
}

Étude continue

Pour en savoir plus, consultez les ressources suivantes, qui vont des détails des spécifications à d'autres articles intéressants sur ce sujet: