On experimenting with parallax scrolling using css perspective, transformZ, and preserve-3d, parallax effects can be applied to scrolling containers using hardware acceleration with near perfect performance.
eg:
.parallax {
perspective: 1px;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
transform-style: preserve-3d;
}
// any containing html tag in the middle
.is-container {
transform-style: preserve-3d;
}
.parallax__layer--base {
transform: translateZ(0);
}
.parallax__layer--back {
transform: translateZ(-1px);
}
On further testing, trying to apply this logic to the main scrolling element (body, html, root) it is proving quite impossible to get full browser support.
Applying this to the equivalent styles of .parallax to the <HTML> tag and applying the equivalent of .is-container to body will break the effect on Chrome and Safari.
Applying th equivalent styles of .parallax to the <BODY> tag will preserve the effect so long as HTML is set to overflow: hidden. Whilst this might seem like a workable approach it then breaks native scroll implementations in browsers. For example, Apple Mobile Safari with the address bar at the bottom runs an animation based on user scroll to hide the address bar as the user scrolls down. This will not take place with BODY acting as the scroll element. Apple Desktop Safari equally floods the background of the highest most positioned item into the app chrome. This will also be broken by this approach.
The google demo https://developer.chrome.com/blog/performant-parallaxing for example also is not using the root / html of page and therefore disables this behaviour of the Apple Safari browsers.
I have tried every combination of applying the equivalent styles of .parallax to :root, HTML and Body but have not been able to rectify this problem. Am I missing something or is this as yet not achievable.
You obviously won't be able to see the issue if we use a wrapper like jsFiddle or CodePen.
Here is some code if you want to reproduce it.
<html>
<style type="text/css">
:root {
height: 100vh !important;
overflow-x: hidden;
overflow-y: auto;
perspective: 1px;
transform-style: preserve-3d;
}
body,
html {
transform-style: preserve-3d;
}
.test {
transform: translate3d(0, 0, -2px);
transform-style: preserve-3d;
}
h1 {
font-size: 120px;
padding: 60px;
text-align: center;
}
</style>
<body>
<h1>HI 1</h1>
<h1>HI 2</h1>
<h1 class="test">HI 3</h1>
<h1>HI 4</h1>
<h1>HI 5</h1>
<h1>HI 6</h1>
<h1>HI 7</h1>
<h1>HI 8</h1>
<h1>HI 9</h1>
<h1>HI 10</h1>
</body>
</html>
As you may see, the perspective element is maintained but the parallax effect is lost.
If we do this however, the parallax is maintained but we broke native listeners to the root scroll element such as Apple Mobile Safari.
<style type="text/css">
:root {
height: 100vh !important;
overflow: hidden;
}
body {
height: 100vh !important;
overflow-x: hidden;
overflow-y: auto;
perspective: 1px;
transform-style: preserve-3d;
}
body,
html {
transform-style: preserve-3d;
}
.test {
transform: translate3d(0, 0, -2px);
transform-style: preserve-3d;
}
h1 {
font-size: 120px;
padding: 60px;
text-align: center;
}
</style>
Update 6 February 2024
// NOT THE ANSWER -- BUT AN ALTERNATIVE //
This is a shortcoming of the capabilities inside css as incorporated by browsers at this moment in time and is likely to remain so for browser compatibility reasons. The only way is to override what is the scrolling element "viewport" to an element we can control. For most browsers this is syphoned up from the html selector and can be accessed there or via the :root selector.
Whilst perspective is taken into account by html, the parallax effect will not work. Translate Z will but something about the way this works flattens the document after again. No amount of preserve-3d applied to html, body and all subsequent containers will change this. The effect of translateZ will work but the parallax effect of it will not.
Therefore the solution is to play with translateY via javascript to achieve the objective. Most parallax libraries are expensive in terms of performance, something that becomes all to evident when used on mobile.
However there are ways to refine this process. Ensuring that effects only take place when visible, ensuring that effects target css 3 transformations not position arguments and ensuring that calls are controlled via an interval on the scroll listener (edit below) are paramount. Making such a script genuinely usable also requires some effort. How do you determine what the zero point is for an element in the scroll view, how much its position can be altered below this point and above this point, it's speed, direction etc..
To solve all of these problems I started with a light weight javascript named mini parallax. https://www.cssscript.com/demo/mini-horizontal-vertical-parallax/.
It became clear that it relied on knowing the genuine offset of any element to determine the zero point. I overcame this by establishing the zero point as the exact middle point of any element when in the middle of the viewport unless that number was less than 0 in which case 0 or greater than the document height in which case the document height.
I removed most of it's unnecessary helpers such as scaling, calculating etc.. And finally rebuilt it entirely. Minified it is less than 500 bytes. Performance in browser is perfect and in mobile it averages 28 FPS with minor fluctuations occasionally. Not perfect but hardly noticeable.
Furthermore that zero point can be modified in multiples of the window height (vh) via data-* properties as can the speed and the minimum and maximum range all in multiples of vh assuming 0 is the centre of the element when visibly at the centre of the screen.
Any html element with .parallax will be included. Additional properties are controlled via data-* properties.
data-parallax-origin: // default 0 (centre of the element when that element is in centre of the visible screen, eg. 0.5 would be centre of the element when three-quarters of the way down the screen.
data-parallax-speed: // default 0.25 (the element scrolls .25 faster that the scroll view)
data-parallax-max: // default 1 (when the effect starts happening, eg. 1 means when the top of the element has reached the bottom of the scrollable window).
data-parallax-min: // default -1 (when the effect stops happening).
data-parallax-axis: // default "y" (when set to x elements would transform in the horizontal axis as the window scrolled in the vertical axis).
Any element can be nested inside any other element and there are no limitations other than performance.
I have not found a single parallax library more efficient than this in terms of performance that is publicly available.
I will therefore publish it shortly along with 12 other scripts and modifiers that answer most of the questions I have asked of late on StackOverFlow.
// NOT THE ANSWER -- BUT AN ALTERNATIVE (Improved) //
Don't use an interval. Use requestAnimationFrame. Contrary to another answer written on a different post about how this has no effect on css transform properties, they are a bit a prat. If dealing with a transition they would be correct but not a transform.
requestAnimationFrame(function(){ // stuff // });
.. renders only when a the browser intends to paint the result of the layout.
Therefore:
Calculate everything you need to calculate on DOM load. Calculate it again if the screenSize changes and store all of this information in a variable (data-• properties if you must).
Run a function on scroll that measures where an element should transform to but don't set it, just store it in a
variable.If any element in your variable is different than the value it should now be, store them all, id's to them all or better still indices to them all in
anotherVariable.in that
anotherVariableis not emptyrequestAnimationFrame(function(){ // and update the transform property of all affected elements }
This will work out the position before paining. It's not perfect because scrollListener is hefty but it is better than an interval that also got overloaded with and inferred a delayed value.
QED :: code will come later.