Grids are truly magical. There’s so many different kind of things we can do with them; layout-wise and scroll-wise. Some time ago, I came across Giulia Tonon’s amazing website. The unique design is enhanced by the exquisite motion of the columns: while scrolling, the middle column scrolls one way, while the outer ones scroll the other way.
This is something that I thought would be interesting to build upon using Locomotive Scroll and combine it with a little idea of flying grid items. Once we click on a grid item, it animates to the center of the screen while scaling up. The other grid items move to their respective positions in the row of thumbnails beneath the main image. This kind of animation is highly inspired by the work of Aristide Benoist who is the master of delicate view switching motions and unique layout animations.
This is the initial view:
When clicking on a image, we move it to the center and animate all other images in the viewport to the little thumbnail navigation:
And this is how all the motion flow looks like:
Please be aware that this experiment is mostly a mockup (no “real” thumbnail navigation for this one)!
I really hope you find this inspirational! Thank you for checking by!
It’s fascinating which magical effects you can add to a website when you experiment with vertex displacement. Today we’d like to share a method with you that you can use to create your own WebGL shader animation linked to scroll progress. It’s a great way to learn how to bind shader vertices and colors to user interactions and to find the best flow.
For our flexible scroll stage, we quickly create three sections with Pug. By adding an element to the sections array, it’s easy to expand the stage.
index.pug:
.scroll__stage
.scroll__content
- const sections = ['Logma', 'Naos', 'Chara']
each section, index in sections
section.section
.section__title
h1.section__title-number= index < 9 ? `0${index + 1}` : index + 1
h2.section__title-text= section
p.section__paragraph The fireball that we rode was moving – But now we've got a new machine – They got music in the solar system
br
a.section__button Discover
The sections are quickly formatted with Sass, the mixins we will need later.
Now we write our ScrollStage class and set up a scene with Three.js. The camera range of 10 is enough for us here. We already prepare the loop for later instructions.
We create a mesh, assign a icosahedron geometry and set the blending of its material to additive for loud colors. And – I like the wireframe style. For now, we set the value of all uniforms to 0 (uOpacity to 1). I usually scale down the mesh for portrait screens. With only one object, we can do it this way. Otherwise you better transform the camera.position.z.
In the vertex shader (which positions the geometry) and fragment shader (which assigns a color to the pixels) we control the values of the uniforms that we will get from the scroll position. To generate an organic randomness, we make some noise. This shader program runs now on the GPU.
To find your preferred settings, you can set up a dat.gui for example. I’ll show you another approach here, in which you can combine two (or more) parameters to intuitively find a cool flow of movement. We simply connect the uniform values with the normalized values of the mouse event and log them to the console. As we use this approach only for development, we do not call rAF (requestAnimationFrames).
Once we have chosen the start and end values, it’s easy to attach them to the scroll position. In this example, we want to drop the purple mesh through the blue section so that it is subsequently soaked in blue itself. We increase the frequency and the strength of our vertex displacement. Let’s first enter this values in our settings and update the mesh material. We normalize scrollY so that we can get the values from 0 to 1 and make our calculations with them.
To render the shader only while scrolling, we call rAF by the scroll listener. We don’t need the mouse event listener anymore.
To improve performance, we add an overwrite to the GSAP default settings. This way we kill any existing tweens while generating a new one for every frame. A long duration renders the movement extra smooth. Once again we let the object rotate slightly with the scroll movement. We iterate over our settings and GSAP makes the music.
In this week’s update, the CSS :not pseudo class can accept complex selectors, how to disable smooth scrolling when using “Find on page…” in Chrome, Safari’s support for there media attribute on <video> elements, and the long-awaited debut of the path() function for the CSS clip-path property.
Let’s jump into the news…
The enhanced :not() pseudo-class enables new kinds of powerful selectors
After a years-long wait, the enhanced :not() pseudo-class has finally shipped in Chrome and Firefox, and is now supported in all major browser engines. This new version of :not() accepts complex selectors and even entire selector lists.
For example, you can now select all <p> elements that are not contained within an <article> element.
/* select all <p>s that are descendants of <article> */
article p {
}
/* NEW! */
/* select all <p>s that are not descendants of <article> */
p:not(article *) {
}
In another example, you may want to select the first list item that does not have the hidden attribute (or any other attribute, for that matter). The best selector for this task would be :nth-child(1 of :not([hidden])), but the of notation is still only supported in Safari. Luckily, this unsupported selector can now be re-written using only the enhanced :not() pseudo-class.
/* select all non-hidden elements that are not preceded by a non-hidden sibling (i.e., select the first non-hidden child */
:not([hidden]):not(:not([hidden]) ~ :not([hidden])) {
}
The HTTP Refresh header can be an accessibility issue
The HTTP Refresh header (and equivalent HTML <meta> tag) is a very old and widely supported non-standard feature that instructs the browser to automatically and periodically reload the page after a given amount of time.
<!-- refresh page after 60 seconds -->
<meta http-equiv="refresh" content="60">
According to Google’s data, the <meta http-equiv="refresh"> tag is used by a whopping 2.8% of page loads in Chrome (down from 4% a year ago). All these websites risk failing several success criteria of the Web Content Accessibility Guidelines (WCAG):
If the time interval is too short, and there is no way to turn auto-refresh off, people who are blind will not have enough time to make their screen readers read the page before the page refreshes unexpectedly and causes the screen reader to begin reading at the top.
However, WCAG does allow using the <meta http-equiv="refresh"> tag specifically with the value 0 to perform a client-side redirect in the case that the author does not control the server and hence cannot perform a proper HTTP redirect.
How to disable smooth scrolling for the “Find on page…” feature in Chrome
CSS scroll-behavior: smooth is supported in Chrome and Firefox. When this declaration is set on the <html> element, the browser scrolls the page “in a smooth fashion.” This applies to navigations, the standard scrolling APIs (e.g., window.scrollTo({ top: 0 })), and scroll snapping operations (CSS Scroll Snap).
Unfortunately, Chrome erroneously keeps smooth scrolling enabled even when the user performs a text search on the page (“Find on page…” feature). Some people find this annoying. Until that is fixed, you can use Christian Schaefer’s clever CSS workaround that effectively disables smooth scrolling for the “Find on page…” feature only.
In the following demo, notice how clicking the links scrolls the page smoothly while searching for the words “top” and “bottom” scrolls the page instantly.
Safari still supports the media attribute on video sources
With the HTML <video> element, it is possible to declare multiple video sources of different MIME types and encodings. This allows websites to use more modern and efficient video formats in supporting browsers, while providing a fallback for other browsers.
In the past, browsers also supported the media attribute on video sources. For example, a web page could load a higher-resolution video if the user’s viewport exceeded a certain size.
It is not appropriate for choosing between low resolution and high resolution because the environment can change (e.g., the user might fullscreen the video after it has begun loading and want high resolution). Also, bandwidth is not available in media queries, but even if it was, the user agent is in a better position to determine what is appropriate than the author.
Scott Jehl (Filament Group) argues that the removal of this feature was a mistake and that websites should be able to deliver responsive video sources using <video> alone.
For every video we embed in HTML, we’re stuck with the choice of serving source files that are potentially too large or small for many users’ devices … or resorting to more complicated server-side or scripted or third-party solutions to deliver a correct size.
Scott has written a proposal for the reintroduction of media in video <source> elements and is welcoming feedback.
The CSS clip-path: path() function ships in Chrome
It wasn’t mentioned in the latest “New in Chrome 88” article, but Chrome just shipped the path() function for the CSS clip-path property, which means that this feature is now supported in all three major browser engines (Safari, Firefox, and Chrome).
The path() function is defined in the CSS Shapes module, and it accepts an SVG path string. Chris calls this the ultimate syntax for the clip-path property because it can clip an element with “literally any shape.” For example, here’s a photo clipped with a heart shape:
Back when we released the v17 design (we’re on v18 now) of this site. I added html { scroll-behavior: smooth; } to the CSS. Right away, I got comments like this (just one example):
… when you control+f or command+f and search on CSS-Tricks, it’ll scroll very slowly instead of snapping to the result, which makes finding information and keywords on CSS-Tricks much slower. As someone who uses this shortcut frequently, this is a usability issue for me.
Not terribly long after, I just removed it. I didn’t feel that strongly about it, and the fact that you have almost zero control over it, made me just can the idea.
I see it come up as a “CSS tip” a lot, so I chimed in with my experience:
After mentioning that, Christian Schaefer chimed in with a great idea:
Smooth scrolling is consequently applied to everything. Always. Even when cycling through the browser’s page search results. At least that’s the case for Chromium. So for the page search it would be desirable for the browser to make an exception to that rule and to deactivate smooth scrolling. Until the Chromium team fixes it, here is a trick how to solve the problem on your own with a little bit of extra CSS and HTML.
I’m not sure if Chrome (or any other browser) would consider that a bug or not. I doubt it’s specced since find-on-page isn’t really a web technology feature. But anyway, I much prefer find-on-page without it.
html:focus-within {
scroll-behavior: smooth;
}
It mostly works. The bummer part about it is situations like this…
That will jump the page down. With scroll-behavior: smooth; in place, that’s kinda nice. But <h2> is typically not a “focusable” element. So, with the trick above, there is now no focus within <html> anymore, and the smooth scrolling is lost. If you want to preserve that, you’d have to do:
Hello everyone, introducing myself a little bit first, I’m Luis Henrique Bizarro, I’m a Senior Creative Developer at Active Theory based in São Paulo, Brazil. It’s always a pleasure to me having the opportunity to collaborate with Codrops to help other developers learn new things, so I hope everyone enjoys this tutorial!
In this tutorial I’ll explain you how to create an auto scrolling infinite image gallery. The image grid is also scrollable be user interaction, making it an interesting design element to showcase works. It’s based on this great animation seen on Oneshot.finance made by Jesper Landberg.
I’ve been using the technique of styling images first with HTML + CSS and then creating an abstraction of these elements inside WebGL using some camera and viewport calculations in multiple websites, so this is the approach we’re going to use in this tutorial.
The good thing about this implementation is that it can be reused across any WebGL library, so if you’re more familiar with Three.js or Babylon.js than OGL, you’ll also be able to achieve the same results using a similar code, when it’s about shading and scaling the plane meshes.
So let’s get into it!
Implementing our HTML markup
The first step is implementing our HTML markup. We’re going to use <figure> and <img> elements, nothing special here, just the standard:
<div class="demo-1__gallery">
<figure class="demo-1__gallery__figure">
<img class="demo-1__gallery__image" src="images/demo-1/1.jpg">
</figure>
<!-- Repeating the same markup until 12.jpg. -->
</div>
Setting our CSS styles
The second step is styling our elements using CSS. One of the first things I do in a website is defining the font-size of the html element because I use rem to help with the responsive breakpoints.
This comes in handy if you’re doing creative websites that only require two or three different breakpoints, so I highly recommend starting using it if you haven’t adopted rem yet.
One thing I’m also using is calc() with the size of the designs. In our tutorial we’re going to use 1920 as our main width, scaling our font-size depending on the screen size of 100vw. This results in 10px at a 1920px screen, for example:
html {
font-size: calc(100vw / 1920 * 10);
}
Now let’s style our grid of images. We want to freely place our images across the screen using absolute positioning, so we’re just going to set the height, width and left/top styles across all our demo-1 classes:
Note that we’re hiding the visibility of our HTML, because it’s not going to be visible for the users since we’re going to load these images inside the <canvas> element. But below you can find a screenshot of what the result will look like.
Creating our OGL 3D environment
Now it’s time to get started with the WebGL implementation using OGL. First let’s create an App class that is going to be the entry point of our demo and inside of it, let’s also create the initial methods: createRenderer, createCamera, createScene, onResize and our requestAnimationFrame loop with update.
In our createRenderer method, we’re initializing one renderer with alpha enabled, storing our GL context (this.renderer.gl) reference in the this.gl variable and appending our <canvas> element to our document.body.
In our createCamera method, we’re just creating a new Camera and setting some of its attributes: fov and its z position.
In our createScene method, we’re using the Transform class, that is the representation of a new scene that is going to contain all our planes that represent our images in the WebGL environment.
The onResize method is the most important part of our initial setup. It’s responsible for three different things:
Making sure we’re always resizing the <canvas> element with the correct viewport sizes.
Updating our this.camera perspective dividing the width and height of the viewport.
Storing in the variable this.viewport, the value representations that will help to transform pixels into 3D environment sizes by using the fov from the camera.
The approach of using the camera.fov to transform pixels in 3D environment sizes is an approach used very often in multiple WebGL implementations. Basically what it does is making sure that if we do something like: this.mesh.scale.x = this.viewport.width; it’s going to make our mesh fit the entire screen width, behaving like width: 100%, but in 3D space.
And finally in our update, we’re setting our requestAnimationFrame loop and making sure we keep rendering our scene.
Create our reusable geometry instance
It’s a good practice to keep memory usage low by always reusing the same geometry reference no matter what WebGL library you’re using. To represent all our images, we’re going to use a Plane geometry, so let’s create a new method and store this new geometry inside the this.planeGeometry variable.
import { Renderer, Camera, Transform, Plane } from 'ogl'
createGeometry () {
this.planeGeometry = new Plane(this.gl)
}
Select all images and create a new class for each one
Now it’s time to use document.querySelector to select all our images and create one reusable class that is going to represent our images. (We’re going to create a single Media.js file later.)
createMedias () {
this.mediasElements = document.querySelectorAll('.demo-1__gallery__figure')
this.medias = Array.from(this.mediasElements).map(element => {
let media = new Media({
element,
geometry: this.planeGeometry,
gl: this.gl,
scene: this.scene,
screen: this.screen,
viewport: this.viewport
})
return media
})
}
As you can see, we’re just selecting all .demo-1__gallery__figure elements, going through them and generating an array of `this.medias` with new instances of Media.
Now it’s important to start attaching this array in important pieces of our setup code.
Let’s first include all our media inside the method onResize and also call media.onResize for each one of these new instances:
And inside our update method, we’re going to call media.update() as well:
if (this.medias) {
this.medias.forEach(media => media.update())
}
Setting up our Media.js file and class
Our Media class is going to use Mesh, Program and Texture classes from OGL to create a 3D plane and attribute a texture to it, which in our case is going to be our images.
In our constructor, we’re going to store all variables that we need and that were passed in the new Media() initialization from index.js:
import { Mesh, Program, Texture } from 'ogl'
import fragment from 'shaders/fragment.glsl'
import vertex from 'shaders/vertex.glsl'
export default class {
constructor ({ element, geometry, gl, scene, screen, viewport }) {
this.element = element
this.image = this.element.querySelector('img')
this.geometry = geometry
this.gl = gl
this.scene = scene
this.screen = screen
this.viewport = viewport
this.createMesh()
this.createBounds()
this.onResize()
}
}
In our createMesh method, we’ll load the image texture using the this.image.src attribute, then create a new Program, which is basically a representation of the material we’re applying to our Mesh. So our method looks like this:
Looks pretty simple, right? After we generate a new Mesh, we’re setting the plane as children of this.scene, so we’re including our mesh inside our main scene.
As you’ve probably noticed, our Program receives fragment and vertex. These both represent the shaders we’re going to use on our planes. For now, we’re just using simple implementations of both.
In our vertex.glsl file we’re getting the uv and position attributes, and making sure we’re rendering our planes in the right 3D world position.
In our fragment.glsl file, we’re receiving a tMap texture, as you can see in the tMap: { value: texture } declaration, and rendering it in our plane geometry:
The createBounds method is important to make sure we’re positioning and scaling our planes in the correct DOM elements positions, so it’s basically going to call for this.element.getBoundingClientRect() to get the right position of our planes, and then after that using these values to calculate the 3D values of our plane.
As you’ve probably noticed, the calculations for scale.x and scale.y are going to stretch our plane to make it the same width and height of the <img> elements. And the position.x and position.y takes the offset from the element and makes our translate our planes to the correct x and y axis in 3D.
And let’s not forget our onResize method, which is basically going to call createBounds again to refresh our getBoundingClientRect values and make sure we keep our 3D implementation responsive as well.
onResize (sizes) {
if (sizes) {
const { screen, viewport } = sizes
if (screen) this.screen = screen
if (viewport) this.viewport = viewport
}
this.createBounds()
}
This is the result we’ve got so far.
Implement cover behavior in fragment shaders
As you’ve probably noticed, our images are stretched. It happens because we need to make proper calculations in the fragment shaders in order to have a behavior like object-fit: cover; or background-size: cover; in WebGL.
I like to use an approach to pass the images’ real sizes and do some ratio calculations inside the fragment shader, so let’s adapt our code to this approach. So in our Program, we’re going to pass two new uniforms called uPlaneSizes and uImageSizes:
Before we implement our infinite logic, it’s good to start making scrolling work properly. In our setup code, we have included a onWheel method, which is going to be used to lerp some variables and make our scroll butter smooth.
In our constructor from index.js, let’s create the this.scroll object with these variables:
Now let’s update our onWheel implementation. When working with wheel events, it’s always important to normalize it, because it behaves differently based on the browser, I’ve been using normalize-wheel library to help on it:
Let’s also create our lerp utility function inside the file utils/math.js:
export function lerp (p1, p2, t) {
return p1 + (p2 - p1) * t
}
And now we just need to lerp from the this.scroll.current to the this.scroll.target inside the update method. And finally pass it to the media.update() methods:
The approach of making an infinite scrolling logic is basically repeating the same grid over and over while the user keeps scrolling your page. Since the user can scroll up or down, you also need to take under consideration what direction is being scrolled, so overall the algorithm should work this way:
If you’re scrolling down, your elements move up — when your first element isn’t on the screen anymore, you should move it to the end of the list.
If you’re scrolling up, your elements move to down — when your last element isn’t on the screen anymore, you should move it to the start of the list.
To explain it in a visual way, let’s say we’re scrolling down and the red area is our viewport and the blue elements are not in the viewport anymore.
When we are in this state, we just need to move the blue elements to the end of our gallery grid, which is the entire height of our gallery: 295rem.
Let’s include the logic for it then. First, we need to create a new variable called this.scroll.last to store the last value of our scroll, this is going to be checked to give us up or down strings:
Then we need to get the total gallery height and transform it to 3D dimensions, so let’s include a querySelector of .demo-1__gallery and call the createGallery method in our index.js constructor.
onResize (sizes) {
if (sizes) {
const { height, screen, viewport } = sizes
if (height) this.height = height
if (screen) this.screen = screen
if (viewport) this.viewport = viewport
}
}
Now we’re going to include the logic to move our elements based on their viewport position, just like our visual representation of the red and blue rectangles.
If the idea is to keep summing up a value based on the scroll and element position, we can achieve this by just creating a new variable called this.extra = 0, this is going to store how much we need to sum (or subtract) of our media, so in our constructor let’s include it:
Finally, the only thing left now is updating the this.extra variable inside our update method, making sure we’re adding or subtracting the this.height depending on the direction.
Since we’re working in 3D space, we’re dealing with cartesian coordinates, that’s why you can notice we’re dividing most things by two (ex: this.viewport.heighht / 2). So that’s also the reason why we had to do a different logic for the this.isBefore and this.isAfter checks.
Awesome, we’re almost finishing our demo! That’s how it looks now, pretty cool to have it endless right?
Including touch events
Let’s also include touch events, so this demo can be more responsive to user interactions! In our addEventListeners method, let’s include some window.addEventListener calls:
Done! Now we also have touch events support enabled for our gallery.
Implementing direction-aware auto scrolling
Let’s also implement auto scrolling to make our interaction even better. In order to achieve that we just need to create a new variable that will store our speed based on the direction the user is scrolling.
So let’s create a variable called this.speed in our index.js file:
constructor () {
this.speed = 2
}
This variable is going to be changed by our down and up validations we have in our update loop, so if the user is scrolling down, we’re going to keep the speed as 2, if the user is scrolling up, we’re going to replace it with -2, and before that we will sum this.speed to the this.scroll.target variable:
Now let’s make everything even more interesting, it’s time to play a little bit with shaders and distort our planes while the user is scrolling through our page.
First, let’s update our update method from index.js, making sure we expose both current and last scroll values to all our medias, we’re going to do a simple calculation with them.
As you can probably notice, we’re going to need to set uViewportSizes in our onResize method as well, since this.viewport changes when we resize, so to keep this.viewport.width and this.viewport.height up to date, we also need to include the following lines of code in onResize:
onResize (sizes) {
if (sizes) {
const { height, screen, viewport } = sizes
if (height) this.height = height
if (screen) this.screen = screen
if (viewport) {
this.viewport = viewport
this.plane.program.uniforms.uOffset.value = [this.viewport.width, this.viewport.height]
}
}
}
Remember the this.scroll update we’ve made from index.js? Now it’s time to include a small trick to generate a speed value inside our Media.js:
We’re basically checking the difference between the current and last values, which returns us some kind of “speed” of the scrolling, and dividing it by the this.screen.width, to keep our effect value behaving correctly independently of the width of our screen.
Finally now it’s time to play a little bit with our vertex shader. We’re going to bend our planes a little bit while the user is scrolling through the page. So let’s update our vertex.glsl file with this new code:
That’s it! Now we’re also bending our images creating an unique type of effect!
Explaining a little bit of the shader logic: basically what’s implemented in the newPosition.z line is taking into consideration the uViewportSize.y, which is our height from the viewport and the current position.y of our plane, getting the division of both and multiplying by PI that we defined on the very top of our shader file. And then we use the uStrength which is the strength of the bending, that is tight with our scrolling values, making it bend based on how faster you scroll the demo.
That’s the final result of our demo! I hope this tutorial was useful to you and don’t forget to comment if you have any questions!
If you use Locomotive Scroll, you might have heard about the new update that includes support for horizontal layouts. I’m a big fan of the library, so I couldn’t wait to try out some layouts and animations with the new feature.
The main concept behind these layouts is to play around with animations that feel and work well for scrolling to the sides. This includes animating images and text depending on the direction we scroll. Moving things up and down for example or skewing them can create a really interesting dynamic effect.
By using a structure of the images where an outer element has overflow: hidden and the inner one moves, we can create a cool parallaxed motion that adds depth to the whole layout.
I really hope you enjoy these experiments and find them useful Thank you for stopping by and let me know what you create with Locomotive Scroll @crnacura.
In this article, I’ll show you how to build a parallax slider with a fun reveal animation. I’ll be using GSAP, CSS Grid and Flexbox and I’ll assume that you have some basic knowledge on how to use these techniques. Besides that, I’ll be using a custom smooth scroll based on Virtual Scroll for a better experience.
The article is split up in 3 steps:
The hover animation
The open/expand animation
The slider scroll/drag parallax effect
For a better understanding I added motion videos for each step.
1. Hover animation
When we hover the button on the top right, 3 placeholder images will animate in the viewport from the right side.
We have a this.dom object where we store all the DOM elements such as the images. When initializing the app we call the setHoverAnimation which creates a GSAP timeline that is set default to paused. In the timeline we will animate the 3 images into the viewport so they’re partly visible. The user will then have the impression that they can be expanded on click.
To trigger the animation when hovering the button, we created 2 events (handleMouseenter and handleMouseleave) which will play the GSAP timeline or simply reverse the animation.
When we click the button on the top right, after hovering, the 3 placeholder images will animate to a 3 column grid. There will be more than 3 items in the slider, but only the first 3 will be visible in the viewport so it’s not necessary to animate the other items. Once the 3 placeholder items are in the correct position we can display the actual slider items underneath and remove the placeholder items.
Animation
First we have to calculate the position that the placeholder items will have to animate to. Subtracting the left position of the placeholder items with the left position of the slider items will get you the correct x position. To get the y position I’m doing the exact same thing, but use the top bounds instead.
The placeholder items are smaller than the slider items so we have to scale them up. To calculate the scale value we will just divide the width of the placeholder items by the width of one of the slider items (they are all the same size).
The intersectX1 X2 X3 values are used to set the initial x position of the images inside its container. Each image in the slider will have its own x position stored. The x position will be used to animate the slider images inside its container, this will create the parallax effect.
A new GSAP timeline will be created for the expand animation. Once the timeline is completed setHoverAnimation will be fired so the placeholder items are reset and ready to animate again. Also we will start the reveal animation of the slider elements such as the text on the items and the close button, but we will not go into the text animations. You can find that in the final code.
The images are scaled up in its container that will have `overflow: hidden`. Once you move the slider, the images that are in the viewport will move with a slightly different speed.
We will be using a smooth scroll on top of Virtual Scroll, but we will not go into how to set this up (please check the documentation). With interpolation, we can achieve this smooth parallax scrolling effect.
Once again we have a this.dom object where we will store all the DOM elements that we need.
To keep track of the scroll progress we created a simple progress bar which defines how much we moved the slider. We simply calculate a value between 0 and 1 that represents the x position of the slider container. This value will be updated in a requestAnimationFrame and used to animate the scaleX value of the progress bar.
Before we start the animation of the parallax effect on the images we first store some data of each slider item in the array this.items. This array wil contain the image element, the bounds and the x position of the image.
Now we’re finally ready to create the parallax effect on the images. The code below will be executed in a render function in a requestAnimationFrame. In this function we will be using the interpolated value of our scroll (scroll.state.last).
For a better performance we will only animate the images that are in the viewport. To do so we will detect which items are visible in the viewport.
If the item is visible in the viewport, we will calculate a value between 0 and 100 (percentage) that indicates how much of the target element is actually visible within the viewport.
I hope this has been not too difficult to follow and that you have gained some insight into creating this parallax slider.
If you would like to see this slider live in action, let’s have a look at the architectural website for Nieuw Bergen by Gewest13. It’s used to showcase their seven buildings.
Please let me know if you have any questions @rluijtenant.
Today I want to share a simple intro animation with you. The concept is to first show an image stack and then animate each image to its position in the grid (or any other layout).
For the first demo, I actually used a simple serial layout and added some smooth scrolling with Locomotive Scroll. The animations are powered by GSAP.
The other day I looked at the beautiful website of Elias & Valentin and fell in love with that nice tilted image grid that animates on scroll. The look and feel of a grid like that is super fashionable right now so I wanted to explore this and other layout variations I stumbled upon.
For the scroll animations I used Locomotive Scroll which is a really fantastic library when it comes to smooth scrolling and on-scroll animations.
Check out the GitHub repo of Locomotive Scroll to see how simple it is to use the library.
I really hope you find this set interesting and useful
I also wanted to add some smooth scrolling and on-scroll animations, so I’ve used Locomotive Scroll. The beautiful images are by DeMorris Byrd.
This is highly experimental and it turned out to be a complex process. But I hope it gives you some of sort idea and entry point of how to pull off these kind of animations without touching the width and height of an element.
The main idea behind this technique is to scale an element and then counter-scale the child. Paul Lewis and Stephen McGruer show how to do that on a menu using expand and collapse animations. Avoiding animating the width and height of an element helps keep performance in check.
So what we do is to initially set the scale of the content__intro wrapper to a value that will make it shrink to an exact size. Then we set a counter scale to the image. This will make the image maintain the same size as before. Then, we add another scale to the image, shrinking it also the to the target size.
Having the initial width and height of an element and also the target dimensions, we can calculate the scale values of the outer wrapper based on this:
1/introTransform.scaleX is the counter scale of the outer wrapper. The second value that we multiply makes sure that we scale the image down to our desired size, just like we did with the outer wrapper before.
And that’s the main idea behind the scaling magic.
I hope this gives you a starting point for these kind of tricky animations! Thank you for checking it out
Since I first became aware of the energy in this community, I felt the urge to be more engaged in this ‘digital avant-garde landscape’ that is being cultivated by the amazing people behind Codrops, Awwwards, CSSDA, The FWA, Webby Awards, etc. That energy has propelled me to set up this new portfolio, which acted as a way of putting my feet into the water and getting used to the temperature.
I see this community being responsible for pushing the limits of what is possible on the web, fostering the right discussions and empowering the role of creative developers and creative designers across the world.
With this in mind, it’s difficult not to think of the great art movements of the past and their role in mediating change. You can easily draw a parallel between this digital community and the Impressionists artists in the last century, or as well the Bauhaus movement leading our society into modernism a few decades ago. What these periods have in common is that they’re pushing the boundaries of what is possible, of what is the new standard, doing so through relentless experimentation. The result of that is the world we live in, the products we interact with, and the buildings we inhabit.
The websites that are awarded today, are so because they are innovating in some aspects, and those innovations eventually become a new standard. We can see that in the apps used by millions of people, in consumer websites, and so on. That is the impact that we make.
I’m not saying that a new interaction featured on a new portfolio launched last week is going to be in the hands of millions of people across the globe in the following week, although constantly pushing these interactions to its limits will scale it and eventually make these things adopted as new standards. This is the kind of responsibility that is in our hands.
Open Source
We decided to be transparent and take a step forward in making this entire project open source so people can learn how to make the things we created. We are both interested in supporting the community, so feel free to ask us questions on Twitter or Instagram (@brunoarizio and @lhbzr), we welcome you to do so!
With the portfolio, we took a meticulous approach to motion and collaborated to devise deliberate interactions that have a ‘realness’ to it, especially on the main page.
The mix of the bending animation with the distortion effect was central to making the website ‘tactile’. It is meant to feel good when you shuffle through the projects, and since it was published we received a lot of messages from people saying how addictive the navigation is.
A lot of my new ideas come from experimenting with shaders and filters in After Effects, and just after I find what I’m looking for — the ‘soul’ of the project — I start to add the ‘grid layer’ and begin to structure the typography and other elements.
In this project, before jumping to Sketch, I started working with a variety of motion concepts in AE, and that’s when the version with the convection bending came in and we decided to take it forward. So we can pretty much say that the project was born from motion, not from a layout in this matter. After the main idea was solid enough, I took it to Sketch, designed a simple grid and applied the typography.
Collaboration
Working in collaboration with Luis was so productive. This is the second (of many to come) projects working together and I can safely say that we had a strong connection from start to finish, and that was absolutely important for the final results. It wasn’t a case in which the designer creates the layouts and hands them over to a developer and period. This was a nuanced relationship of constant feedback. We collaborated daily from idea to production, and it was fantastic how dev and design had this keen eye for perfectionism.
From layout to code we were constantly fine-tuning every aspect: from the cursor kinetics to making overhaul layout changes and finding the right tone for the easing curves and the noise mapping on the main page.
When you design a portfolio, especially your own, it feels daunting since you are free to do whatever you want. But the consequence is that this will dictate how people will see your work, and what work you will be doing shortly after. So making the right decisions deliberately and predicting its impact is mandatory for success.
Technical Breakdown
Luis Henrique Bizarro, Creative Developer — @lhbzr
Motion Reference
This was the video of the motion reference that Bruno shared with me when he introduced me his ideas for his portfolio. I think one of the most important things when starting a project like this with the idea of implementing a lot of different animations, is to create a little prototype in After Effects to drive the developer to achieve similar results using code.
That’s my favorite stack to work with right now; it gives me a lot of freedom to focus on animations and interactions instead of having to follow guidelines of a specific framework.
In this particular project, most of the code was written from scratch using ECMAScript 2015+ features like Classes, Modules, and Promises to handle the route transitions and other things in the application.
In this case study, we’ll be focusing on the WebGL implementation, since it’s the core animation of the website and the most interesting thing to talk about.
1. How to measure things in Three.js
This specific subject was already covered in other articles of Codrops, but in case you’ve never heard of it before, when you’re working with Three.js, you’ll need to make some calculations in order to have values that represent the correct sizes of the viewport of your browser.
In my last projects, I’ve been using this Gist by Florian Morel, which is basically a calculation that uses your camera field-of-view to return the values for the height and width of the Three.js environment.
I usually store these two variables in the wrapper class of my applications, this way we just need to pass it to the constructor of other elements that will use it.
In the embed below, you have a very simple implementation of a PlaneBufferGeometry that covers 75% of the height and width of your viewport using this solution.
2. Uploading textures to the GPU and using them in Three.js
In order to avoid the textures to be processed in runtime while the user is navigating through the website, I consider a very good practice to upload all images to the GPU immediately when they’re ready. On Bruno’s portfolio, this process happens during the preloading of the website. (Kudos to Fabio Azevedo for introducing me this concept a long time ago in previous projects.)
Another two good additions, in case you don’t want Three.js to resize and process the images you’re going to use as textures, are disabling mipmaps and change how the texture is sampled by changing the generateMipmaps and minFilter attributes.
The method .initTexture() was introduced back in the newest versions of Three.js in the WebGLRenderer class, so make sure to update to the latest version of the library to be able to use this feature.
But my texture is looking stretched! The default behavior of Three.js map attribute from MeshBasicMaterial is to make your image fit into the PlaneBufferGeometry. This happens because of the way the library handles 3D models. But in order to keep the original aspect ratio of your image, you’ll need to do some calculations as well.
There’s a lot of different solutions out there that don’t use GLSL shaders, but in our case we’ll also need them to implement our animations. So let’s implement the aspect ratio calculations in our fragment shader that will be created for the ShaderMaterial class.
So, all you need to do is pass your Texture to your ShaderMaterial via the uniforms attribute. In the fragment shader, you’ll be able to use all variables passed via the uniforms attribute.
In Three.js Uniform documentation you have a good reference of what happens internally when you pass the values. For example, if you pass a Vector2, you’ll be able to use a vec2 inside your shaders.
We need two vec2 variables to do the aspect ratio calculations: the image resolution and the resolution of the renderer. After passing them to the fragment shader, we just need to implement our calculations.
In this snippet we’re using template strings to represent the code of our shaders only to keep it simple when using CodeSandbox, but I highly recommend using glslify to split your shaders into multiple files to keep your code more organized in a more robust development environment.
We’re all good now with the images! Our images are preserving their original aspect ratio and we also have control over how much space they’ll use in our viewport.
3. How to implement infinite scrolling
Infinite scrolling can be something very challenging, but in a Three.js environment the implementation is smoother than it’d be without WebGL by using CSS transforms and HTML elements, because you don’t need to worry about storing the original position of the elements and calculate their distance to avoid browser repaints.
Overall, a simple logic for the infinite scrolling should follow these two basic rules:
If you’re scrolling down, your elements move up — when your first element isn’t on the screen anymore, you should move it to the end of the list.
If you’re scrolling up, your elements move to down — when your last element isn’t on the screen anymore, you should move it to the start of the list.
Sounds reasonable right? So, first we need to detect in which direction the user is scrolling.
The variable this.scroll.values.target is responsible for defining to which scroll position the user wants to go. Then the variable this.position.current represents the current position of your scroll, it goes smoothly to the value of the target with the * 0.1 multiplication.
After detecting the direction the user is scrolling towards, we just store the current position to the this.position.previous variable, this way we’ll also have the right direction value inside the requestAnimationFrame.
Now we need to implement the checking method to make our items have the expected behavior based on the direction of the scroll and their position. In order to do so, you need to implement a method like this one below:
The website motion reference has four different animations happening while the user is scrolling:
Movement on the z-axis: the image moves from the back to the front.
Bending on the z-axis: the image bends a little bit depending on its position.
Image scaling: the image scales slightly when moving out of the screen.
Image distortion: the image is distorted when we start scrolling.
My approach to implementing the animations was to use a calculation of the element position divided by the viewport height, giving me a percentage number between -1 and 1. This way I’ll be able to map this percentage into other values inside the ShaderMaterial instance.
The implementation of the z-axis animation is pretty simple, because it can be done directly with JavaScript using this.position.z from Mesh, so the code for this animation looks like this:
The implementation of the bending animation is slightly more complex, we need to use the vertex shaders to bend our PlaneBufferGeometry. I’ve choose distortion as the value to control this animation inside the shaders. Then we also pass two other parameters distortionX and distortionY which controls the amount of distortion of the x and y axis.
We hope you liked the Case Study we’ve written together, if you have any questions, feel free to ask us on Twitter or Instagram (@brunoarizio and @lhbzr), we would be very happy to receive your feedback.
In this tutorial I will show you how to take a couple of established techniques (like tying things to the scroll-offset), and cast them into re-usable components. Composition will be our primary focus.
In this tutorial we will:
build a declarative scroll rig
mix HTML and canvas
handle async assets and loading screens via React.Suspense
We are using React, hooks, Three.js and react-three-fiber. The latter is a renderer for Three.js which allows us to declare the scene graph by breaking up tasks into self-contained components. However, you still need to know a bit of Three.js. All there is to know about react-three-fiber you can find on the GitHub repo’s readme. Check out the tutorial on alligator.io, which goes into the why and how.
We don’t emulate a scroll bar, which would take away browser semantics. A real scroll-area in front of the canvas with a set height and a listener is all we need.
I decided to divide the content into:
virtual content sections
and pages, each 100vh long, this defines how long the scroll area is
scrollTop is written into a reference because it will be picked up by the render-loop, which is carrying out the animations. Re-rendering for often occurring state doesn’t make sense.
A first-run effect synchronizes the local scrollTop with the actual one, which may not be zero.
Building a declarative scroll rig
There are many ways to go about it, but generally it would be nice if we could distribute content across the number of sections in a declarative way while the number of pages defines how long we have to scroll. Each content-block should have:
an offset, which is the section index, given 3 sections, 0 means start, 2 means end, 1 means in between
a factor, which gets added to the offset position and subtracted using scrollTop, it will control the blocks speed and direction
Blocks should also be nestable, so that sub-blocks know their parents’ offset and can scroll along.
const offsetContext = createContext(0)
function Block({ children, offset, factor, ...props }) {
const ref = useRef()
// Fetch parent offset and the height of a single section
const { offset: parentOffset, sectionHeight } = useBlock()
offset = offset !== undefined ? offset : parentOffset
// Runs every frame and lerps the inner block into its place
useFrame(() => {
const curY = ref.current.position.y
const curTop = state.top.current
ref.current.position.y = lerp(curY, (curTop / state.zoom) * factor, 0.1)
})
return (
<offsetContext.Provider value={offset}>
<group {...props} position={[0, -sectionHeight * offset * factor, 0]}>
<group ref={ref}>{children}</group>
</group>
</offsetContext.Provider>
)
}
This is a block-component. Above all, it wraps the offset that it is given into a context provider so that nested blocks and components can read it out. Without an offset it falls back to the parent offset.
It defines two groups. The first is for the target position, which is the height of one section multiplied by the offset and the factor. The second, inner group is animated and cancels out the factor. When the user scrolls to the given section offset, the block will be centered.
We use that along with a custom hook which allows any component to access block-specific data. This is how any component gets to react to scroll.
We want to keep layout and text-related things in the DOM. However, keeping it in sync is a bit of a bummer in Three.js, messing with createElement and camera calculations is no fun.
In three-fiber all you need is the <Dom /> helper (@beta atm). Throw this into the canvas and add declarative HTML. This is all it takes for it to move along with its parents’ world-matrix.
If we strictly divide between layout and visuals, supporting a11y is possible. Dom elements can be behind the canvas (via the prepend prop), or in front of it. Make sure to place them in front if you need them to be accessible.
Responsiveness, media-queries, etc.
While the DOM fragments can rely on CSS, their positioning overall relies on the scene graph. Canvas elements on the other hand know nothing of the sort, so making it all work on smaller screens can be a bit of a challenge.
Fortunately, three-fiber has auto-resize inbuilt. Any component requesting size data will be automatically informed of changes.
You get:
viewport, the size of the canvas in its own units, must be divided by camera.zoom for orthographic cameras
size, the size of the screen in pixels
const { viewport, size } = useThree()
Most of the relevant calculations for margins, maxWidth and so on have been made in useBlock.
Handling async assets and loading screens via React.Suspense
Concerning assets, Reacts Suspense allows us to control loading and caching, when components should show up, in what order, fallbacks, and how errors are handled. It makes something like a loading screen, or a start-up animation almost too easy.
The following will suspend all contents until each and every component, even nested ones, have their async data ready. Meanwhile it will show a fallback. When everything is there, the <Startup /> component will render along with everything else.
In three-fiber you can suspend a component with the useLoader hook, which takes any Three.js loader, then loads (and caches) assets with it.
function Image() {
const texture = useLoader(THREE.TextureLoader, "/texture.png")
// It will only get here if the texture has been loaded
return (
<mesh>
<meshBasicMaterial attach="material" map={texture} />
Adding shader effects and tying them to scroll
The custom shader in this demo is a Frankenstein based on the Three.js MeshBasicMaterial, plus:
The technique is explained in full detail in the article Real-time Multiside Refraction in Three Steps by Jesper Vos. I placed Jesper’s code into a re-usable component, so that it can be mounted and unmounted, taking care of all the render logic. I also changed the shader slightly to enable instancing, which now allows us to draw dozens of these onto the screen without hitting a performance snag anytime soon.
The component reads out block-data like everything else. The diamonds are put into place according to the scroll offset by distributing the instanced meshes. This is a relatively new feature in Three.js.
Wrapping up
This tutorial may give you a general idea, but there are many things that are possible beyond the generic parallax; you can tie anything to scroll. Above all, being able to compose and re-use components goes a long way and is so much easier than dealing with a soup of code fragments whose implicit contracts span the codebase.
Picking up on our last tutorial on how to add smooth scrolling plus image animations to a page, we’d like to explore some more ideas for animations. We’ve made a small set of effects that show how you can apply some interesting transforms to elements like images and text while scrolling the page smoothly.
Attention: Note that the demos are experimental and that we use modern CSS properties that might not be supported in older browsers.
For the demos, we’ve created different (grid) layouts with images that have decorative elements and captions.
We’ve used background images that are wrapped in a division with its overflow set to hidden, so that we can animate the scale or translate of the inner images in some examples. There are many possibilities to explore, for example, rotating the images:
…or adding a blend mode to one of the moving elements:
As you can also see in Jesper Landberg’s smooth scroll with skew effect demo, you can use the acceleration to control the transform amount. So when you scroll faster, the elements distort more.
Here’s a little GIF to show a detail of one of the animations:
Note that when using the scale transform, the animations in Firefox don’t perform so smoothly.
We hope you enjoy this little set and find it inspirational.
Today we want to show you how to add smooth scrolling in HTML with some additional subtle animations on images. With “smooth scrolling” we don’t mean smoothly scrolling to an element, but rather a smoothly animated kind of scrolling behavior. There are many beautiful examples of such smooth scrolling behavior on some recent websites, like Elena Iv-skaya, or Ada Sokół and the stunning site of Rafal Bojar, and many many others. The latter also has a very nice image animation that is synced with the scrolling. This kind of “inner” image animation adds another interesting layer to the whole scroll movement.
Why is this kind of smooth scrolling something you’d like to add to a web page? If you have ever animated something on scroll, you might have experienced that browsers have difficulties in displaying the incoming content jank-free; especially images may show tiny abrupt jumps on scroll. It just feel easy on the eye. To avoid that, we can use the trick of animating the content itself by translating it up or down instead of using the “native” scroll.
Smooth Scrolling
Jesper Landberg created some really great Codepen demos showcasing how smooth scrolling can be applied to different scenarios. The Smooth scroll with skew effect demo shows how to add a skew effect to images while (smooth) scrolling. Here you can also see how smooth scrolling with translating the content works: a content wrapper is set to position fixed with the overflow set to hidden so that its child can be moved. The body will get the height of the content set to it, so that we preserve the scroll bar. When we scroll, the fixed wrapper will stay in place while we animate the inner content. This trick makes a simple yet effective smooth scrolling behavior possible.
The main element will serve as fixed, or “sticky”, container while the [data-scroll] div will get translated.
Inner Image Animation
For the inner image animation we need an image and a parent container that has its overflow set to “hidden”. The idea is to move the image up or down while we scroll. We will work with a background image on a div so that we can control the overflow size better. Mainly, we need to make sure that the image div is bigger than its parent. This is our markup:
Let’s set the styles for these elements. We will use a padding instead of a height so that we can set the right aspect ratio for the inner div which will have the image as background. For this, we use an aspect ratio variable so that we simply need to set the image width and height and leave the calculation to our stylesheet. Read more about this and other brilliant techniques in Apect Ratio Boxes on CSS-Tricks.
We set an image variable for the background image in the item__img-wrap class so that we don’t have to write too many rules. This does not need to be done like that, of course, especially if you’d like support for older browsers that don’t know what variables are. Set it to the item__img directly as background-image instead, if that’s the case.
The div with the background image is the one we want to move up or down on scroll, so we need to make sure that it’s taller than its parent. For that, we define an “overflow” variable that we’ll use in a calculation for the height and the top. We set this variable because we want to be able to easily change it in some modified classes. This allows us to set a different overflow to each image which changes the visual effect subtly.
Now, let’s do the JavaScript part. Let’s start with some helper methods and variables.
const MathUtils = {
// map number x from range [a, b] to [c, d]
map: (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c,
// linear interpolation
lerp: (a, b, n) => (1 - n) * a + n * b
};
const body = document.body;
We will need to get the window’s size, specifically it’s height, for later calculations.
So far we have a reference to the main element (the container that needs to become “sticky”) and the scrollable element (the one we will be translating to simulate the scroll).
Also, we create an array of our item’s instances. We will get to that in a moment.
Now, we want to update the translateY value as we scroll but we might as well want to update other properties like the scale or rotation. Let’s create an object that stores this configuration. For now let’s just set up the translationY.
We will be using interpolation to achieve the smooth scrolling effect. The “previous” and “current” values are the values to interpolate. The current translationY will be a value between these two values at a specific increment. The “ease” is the amount to interpolate. The following formula calculates our current translation value:
The setValue function sets the current value, which in this case will be the current scroll position.
Let’s go ahead and execute this initially on page load to set up the right translationY value.
We set both interpolation values to be the same, in this case the scroll value, so that the translation gets set immediately without the being animated. We just want the animation happening when we scroll the page. After that, we call the layout function which will apply the transformation to our element. Note that the value will be negative since the element moves upwards.
As for the layout changes, we need to:
set the position of the main element to fixed and the overflow to hidden so it sticks to the screen and doesn’t scroll.
set the height of the body in order to keep the scrollbar on the page. It will be the same as the scrollable element’s height.
Now we start our loop function that updates the values as we scroll.
constructor() {
...
requestAnimationFrame(() => this.render());
}
render() {
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].setValue();
this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
}
this.layout();
// for every item
for (const item of this.items) {
// if the item is inside the viewport call it's render function
// this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport
if ( item.isVisible ) {
item.render();
}
}
// loop..
requestAnimationFrame(() => this.render());
}
The only new thing here is the call to the item’s render function which is called for every item that is inside the viewport. This will update the translation of the item’s inner image as we will see ahead.
Since we rely on the scrollable element’s height, we need to preload the images so they get rendered and we get to calculate the right value for the height. We are using the imagesLoaded to achieve this:
After the images are loaded we remove our page loader, get the scroll position (this might not be zero if we scrolled the page before the last refresh) and initialize our SmoothScroll instance.
preloadImages().then(() => {
document.body.classList.remove('loading');
// Get the scroll position
getPageYScroll();
// Initialize the Smooth Scrolling
new SmoothScroll(document.querySelector('main'));
});
So now that the SmoothScroll is covered let’s create an Item class to represent each of the page items (the images).
The logic here is identical to the SmoothScroll class. We create a renderedStyles object that contains the properties we want to update. In this case we will be translating the item’s inner image (this.DOM.image) on the y-axis. The only extra here is that we are defining a maximum value for the translation (maxValue). This value we’ve previously set in our CSS variable –overflow. Also, we assume the minimum value for the translation will be -1*maxVal.
The setValue function works as follows:
When the item’s top value (relative to the viewport) equals the window’s height (item just came into the viewport) the translation is set to the minimum value.
When the item’s top value (relative to the viewport) equals “-item’s height” (item just exited the viewport) the translation is set to the maximum value.
So basically we are mapping the item’s top value (relative to the viewport) from the range [window’s height, -item’s height] to [minVal, maxVal].
Next thing to do is setting the initial values on load. We also calculate the item’s height and top since we’ll need those to apply the function described before.
Let's say you wanted to scroll a web page from top to bottom programmatically. For example, you're recording a screencast and want a nice full-page scroll. You probably can't scroll it yourself because it'll be all uneven and jerky. Native JavaScript can do smooth scrolling. Here's a tiny snippet that might do the trick for you:
But there is no way to control the speed or easing of that! It's likely to be way too fast for a screencast. I found a little trick though, originally published by (I think) Jedidiah Hurt.
The trick is to use CSS transforms instead of actual scrolling. This way, both speed and easing can be controlled. Here's the code that I cleaned up a little:
The idea is to transform a negative top position for the height of the entire document, but subtract the height of what you can see so it doesn't scroll too far. There is a little magic number in there you may need to adjust to get it just right for you.
Smooth scrolling has gotten a lot easier. If you want it all the time on your page, and you are happy letting the browser deal with the duration for you, it's a single line of CSS:
html {
scroll-behavior: smooth;
}
I tried this on version 17 of this site, and it was the second most-hated thing, aside from the beefy scrollbar. I haven't changed the scrollbar. I like it. I'm a big user of scrollbars and making it beefy is extra usable for me and the custom styling is just fun. But I did revert to no smooth scrolling.
As Šime Vidas pointed to in Web Platform News, Wikipedia also tried smooth scrolling:
The recent design for moved paragraphs in mobile diffs called for an animated scroll when clicking from one instance of the paragraph in question to the other. The purpose of this animation is to help the user stay oriented in terms of where the paragraph got moved to.
We initially thought this behavior would benefit Minerva in general (e.g. when using the table of contents to navigate to a page section it would be awesome to animate the scroll), but after trying it out decided to scope this change just to the mobile diffs view for now
I can see not being able to adjust timing being a downside, but that wasn't what made me ditch smooth scrolling. The thing that seemed to frustrate a ton of people was on-page search. It's one thing to click a link and get zoomed to some header (that feels sorta good) but it's another when you're trying to quickly pop through matches when you do a Find on the page. People found the scrolling between matches slow and frustrating. I agreed.
Surprisingly, even the JavaScript variant of smooth scrolling...
...has no ability to adjust timing. Nor is there a reliable way to detect if the page is actively being searched in order to make UX changes, like turning off smooth scrolling.
Perhaps the largest downside of smooth scrolling is the potential to mismanage focus. Scrolling to an element in JavaScript is fine, so long as you almost move focus to where you are scrolling. Heather Migliorisi covers that in detail here.