Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Pure CSS parallax perspective beyond landscape images

Posted on Jul 3 I researched many tutorials and examples about "parallax scrolling" effects, and I wasn't impressed. Most Parallax examples were not elegant both in a visual way and concerning their code. I often struggled to understand the principle hidden between irrelevant styles and misleading class names. Some "pure CSS" examples still use JavaScript, and most don't care about accessibility, using parallax movements even if the users have set to prefer reduced motion.This (meta-) tutorial shows some experiments and deliberate failures on my way to understand what is going on and how to code a working, robust, accessible, and maintainable perspective effect beyond the typical "awesome landscape" image.Source: Wikimedia Commons: File:Parallax_scrolling_example_scene.gif.What is a "parallax perspective effect" anyway? Simple as it may seem, there seem to be different interpretations of what it is on a website, generalized as an umbrella term for various kinds of scroll-linked movement effects. A popular parallax effect simply resizes full-width hero images to make them slightly larger than they need to be, then adding a subtle scroll-linked movement to make it appear as if we were looking at a distant landscape through a window while moving. This is quite nice, although it does not make much sense if the pictures don't show a distant landscape.Another reason for restricting the effect to hero images is that this circumvents a lot of the problems of more complex scenarios, as we will see when inspecting code demos below.Source: CSS mix-blend-mode and Awesome parallax scrolling by Andrej Sharapov on dribbbleAnother scroll-linked effect that changes the content, like overlaying a photograph over a matching drawing or wireframe model, like in the popular Ivy Chen example, has also been listed as a parallax effect, while it's actually something completely different.According to Wikipedia, parallax is "a displacement or difference in the apparent position of an object viewed along two different lines of sight", and parallax scrolling applies this effect in analog cartoon animation films or "in computer graphics where background images move past the camera more slowly than foreground images, creating an illusion of depth in a 2D scene of distance" like the clouds in classic jump-and-run-games like Super Mario Brothers or The Great Giana Sisters.Source: The Great Giana Sisters game sequence on makeagif.com.Unlike the classic film or computer game examples, where something runs, rides, or flies in a horizontal direction in front of a landscape, websites are typically scrolled down in a vertical direction. I heard different things about perspective effects, from "just add a perspective property" to "the HTML document / body must act as a perspective parent / scrolling container, thus making it necessary to refactor our project and risk undesired side-effects on pages that don't even use the scrolling effect.I found a relatively straightforward codepen by Keith Clark related to his 2014 Pure CSS Parallax Websites that I forked and modified in order to better understand and reduce to its core requirements to generate a minimal template for different kinds of parallax perspective effects that can be added to any project without breaking existing work and without needing to modify existing markup. I only succeeded kind of half way.Some of the "pure CSS" parallax examples actually contain JavaScript, and some aspects might be outdated and obsolete nearly ten years later. But the essence is still the same! Quoting Keith's 2014 blog post:The parallax class is where the parallax magic happens. Defining the height and perspective style properties of an element will lock the perspective to its centre, creating a fixed origin 3D viewport. Setting overflow-y: auto will allow the content inside the element to scroll in the usual way, but now descendant elements will be rendered relative to the fixed perspective. This is the key to creating the parallax effect.Using this technique and replacing the background layer div with a .parallax::after style, we should be able to achieve our goals without JavaScript, global styles and side effects.Why would we even want to do that? While Tailwind and Bootstrap use functional classes everywhere, React projects and content management software introduce illegible generic class names based on random instance hashes, why not write HTML like it's 1999 and add a lot of new div elements to wrap our virtual layers? You might guess from my tone, and my previous articles, that's not how I prefer to code modern websites.Trying to document how and why a decorative module introduced global styles and possible side effects, I could not help but feel I was implementing it in the wrong way.I prefer to introduce new designThat's why I don't think that I succeeded to create a minimal, portable template for a simple parallax scrolling effect yet. But still it might be a step forward - after several steps back and forth, as you can see in this intermediate debugging screenshot, trying to understand and debug adding colorful borders and experimenting with position, height, overflow, and z-index:The new code still has side-effects, as it seems that we need to make its the perspective background layer's height greater than the content, and that clipping with overflow: hidden did somehow break the perspective effect, so we need to give adjacent unrelated elements a higher z-index and define a background color to make sure that they stay on top and remain visually unaffected. Another aspect still true and already mentioned in the "parallax sections" chapter of Keith's good old tutorial:One important rule to keep in mind when grouping elements is, we cannot clip the content of a group. Setting overflow: hidden on a parallax__group will break the parallax effect. Unclipped content will result in descendant elements overflowing, so we need to be creative with the z-index values of the groups to ensure content is correctly revealed/hidden as the visitor scrolls through the document.At least we could, in theory, limit our perspective effect to the parallax container so we don't need to apply global html, body styles. But won't that break continuous body scrolling and add scrollbars to the sections instead?As we can see in the "debug mode" side view of Keith Clark's third demo, we actually have at least three perspective layers: foreground elements, matching section backgrounds (containing background graphics), and a "deep background" which might be the document body or another parent wrapper element containing the effect to make it independent from the rest of the document.But how did that demo prevent multiple scrollbars and make sure that we can scroll the whole document in the usual way using a mouse or touchpad, either swiping, using the mouse wheel, or dragging the scrollbar, or pressing arrow keys? Well, it just didn't! There are two static elements fixed to the top and bottom of the page, but the deep background has been set to height: 100vh; overflow-y: auto and the document body to overflow: hidden.I would like to move the overflow: hidden to our contained section without breaking the perspective effect. But do we really need to add another wrapper, when the parallax container already got the same height and overflow styles? Turns out the body styles are redundant, irrelevant, and obsolete, so I can delete this section when everything works.The point is that demo doesn't use any other content sections before or after the deep background that are not pinned to an absolute position. If it did, it would have the same problem as my interim codepen: double scrollbars and interrupted user experience.At this point, I still failed or refused to understand why I can't simply make the deep background wrapper match its base layer's content height and clip the overflowing decorative background layer without breaking the parallax perspective effect.While the original codepen had multiple foreground elements inside the parallax (group) container, we can add another wrapper around them to make it easier to verify (and hopefully control) its height and behavior.Now I can move any layout styles (just some padding in my minimal example) to the new foreground content wrapper and set margin: 0; padding: 0 to the parallax (group) container.But what happens when I change its height to match the foreground content wrapper and set decorative background, or the parent parallax (group) container, or both, to clip its overflowing content? Will that break my awesome parallax perspective effect again? It will, because we need some scrolling inside the container to make the layers move. If there is no more scrolling, there is no more scrolling effect either!This is the reason that most of the seemingly overengineered tutorials use the whole document body as a deep background and use z-index to make unrelated content areas stay on top of the decorative background layer and have an opaque background-color, while the decorated sections have a transparent one so that the decorations are visible behind its content.Using the document body as a deep background has the advantage that it's already there, without having to add markup outside of our parallax section / module. But we risk to interfere with other modules that might have done other things with the body, so this approach might break unrelated code, especially when used in a large project or a modular CMS environment like WordPress.As our decorative background layer is a child of its parallax group container, its absolute positioning with a four edges set to zero offset ties its location and size where it belongs, while still allowing it to scroll more slowly due to its perspective and distance / Z-axis transformation, resulting in the desired scroll-linked parallax perspective effect.But before accepting those hacky necessities, I want to have another look at the seemingly simple landscape image effects again. Maybe there is some practical middle ground in between those and my previous explorations that still feel a bit like an overly complicated way to achieve the same result as setting background-attachment: fixed for the document body like in this classic W3Schools How-To, although in that case, the background would not move at all, while in the parallax scenario it moves at another speed which can be fine-tuned, plus we can optionally add more than one background layer when using perspective techniques.Advantages of a fixed background attachment: only a few lines of code, very easy to understand, and from a theoretical stance we might even see the fixed background as a special edge case of a perspective scenario which a distance that approximates infinity, much like looking at the moon or the stars that don't seem to move at all when changing perspective, unlike a less distant mountain or a building.CSS Sticky Parallax Sections, a codepen by Ryan Mulligan is another example of a simplified pseduo-perspective approach without actually using three-dimensional transformations, but it's also another example of burying the actual code in beautiful unrelated styling and "hiding" JavaScript in the HTML tab.The sticky background image movement only seems to work with a full-height (100vh) section in this codepen. If we can use the foreground content height instead, then it would be perfect. Maybe we can modify the background's top margin or position inside its sticky container parent to control its movement explicitly.But there is another aspect that makes Ryan's codepen stand out: he cares about accessibility and uses a custom property to disable the perspective movement if the use prefers reduced motion:At that point I still have not been satisfied with mind findings and understanding so far. Every example had some disadvantage that made it hard to use in another context.How can it be so hard to find one tutorial that sums up the concept and provide simple, portable examples for every alternative. Shouldn't there be something like the great grid and flexbox tutorials on MDN going beyond the brief perspective reference?Proceeding my search, googling for "2d parallax approximation css", I came up with a lot of StackOverflow results, and another DEV article, written by Rob O'Leary in 2022:As I "wasted" too much time on this topic already, this probably won't be the perfect roundup with ready-made recipes either - unless I do a complete rewrite some time - but I will publish it anyway, to share and back up my thoughts and findings.My final, clean and minimal (enough) version behaves much like the infamous "awesome landscape", but it allows to use multiple CSS backgrounds or arbitrary DOM content that moves slower than the foreground content.My codepen is mostly based on Ryan Mulligan's tutorial, but it did help to follow Keith Clark's actual 3D approach to understand and compare the advantages and disadvantages of both and see my own conclusions verified when I finally found Rob O'Leary's DEV post. Any parallax approach has its limitations and caveats, and all recent posts seem to agree that we should rely on pure CSS without scroll position listeners to avoid costly callbacks known to cause performance problems.Negative z-index is supported by every modern browser, and so are custom properties (CSS variables). We can even add background-attachment: fixed to improve legacy browser support with only one additional line of code.To avoid undesirable vertical margins when using this technique with variable content height paragraphs instead of hero-style page-height sections, I tried to clip the container and give it a corresponding margin to compensate for the foreground's margin-top: -50vh and the background's height: 100vh, and I changed the latter to min-height: 100vhto make it useful for content of arbitrary length. Note that values below 100vh can make our parallax section overlap other content unless we adjust the foreground's negative top margin accordingly. We can't clip the outer container's overflow-y without clipping the background too early at its bottom, but why? Because position: sticky takes it out of context so its natural height is reduced to 0 – unless we add bottom: 0 to span it to both edges of its parent container? Unfortunately that does not work in this case.Aligning both layers using grid areas (as suggested in StackOverflow answers to position: absolute and parent height? removes the perspective effect, and as far as I know, there is no native CSS property to use an element's scroll position in a calc() function (yet).Pragmatically, we could use JavaScript once, and only once, to determine our foreground layer's height and write it to a custom CSS property that we can use in a max() formula to increase our parallax background's min-height in case 100vh is not enough. It might also be a good idea to provide an absolute minimum height for our section, as the visual effect needs some space to unfold as intended anyway.So we have two variable custom properties, a scaling factor --parallax-scale that can be set to zero for accessibility reasons, and a --parallax-min-height that we can potentially increase based on a one-time measurement when the document has finished rendering (document.addEventListener('DOMContentLoaded' …) but that would go beyond a pure CSS solution.We need some extra space to make the visual effect work properly, but we don't want that to break our layout if the effect is used in a normal text-with-images article context without page-height hero sections. So let's try and adjust our container's top margins and/or hide the overlap below the preceding element.We could compensate the top offset reusing our min-height calculation like this:But this makes both foreground and background scroll together without any offset until they reach the top of the viewport and the sticky position does its trick.So we should either use the same negative top margin on the outer container and make sure its content gets hidden below the preceding element, by defining another negative z-index and adding position: relative, as the implicit default position: static disables any z-index directives.This margin-top still does not match perfectly, as it does not compensate our Y-transformation yet. Without bothering too much about properly converting an integer scaling factor to a rem or pixel value, I'll just hard-code the actual offset like this:Now we're almost done: 😉😂Now there is a side effect: even if there was a CSS selector to target a previous element, our top margin adjustment introduces a new requirement that affects unrelated content. Any element(s) before our parallax scrolling section must have an opaque (non-transparent) background, otherwise the background decoration will shine through.The same is probably also true if we adjust the bottom end of our effect section as well. Although this side effect breaks my idealistic modular requirement, it's "only a single background-color" – and zero margins (we can use padding instead). That's what most (atomic) design systems usually set anyway, just like the typical body { margin: 0; padding: 0, so I think this solution is good enough in practice.Same with the background's height and margin: it looks wrong and broken with a pattern of repeating circles, but nobody would notice when using a set of centered objects or the notorious "awesome landscape" image adjusted with object-fit: cover and faded out below a linear gradient, like every other tutorial and ready-to-use preset does.Now let's remove any debug stuff and see if it works, (re-)test alternative browsers, different viewports, and make sure that there is really is no parallax effect if the user prefers reduced motion!Source: codepen.io/openmindculture/pen/wvQevbdRelevant code snippets:That's it. This is not the perfect best-case-fits-all tutorial that I hoped to write, and neither is my code. All I can say is that it's good enough and that it helped me to better understand the concept and the necessary CSS styles. Hopefully it can help and inspire others as well and show that even senior web developers don't always come up with a perfect solution.Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse Peter Iakovlev - Jun 23 Shittu Olumide - Jun 23 FeeqCoodes - Jun 23 Marc Seitz - Jun 26 Once suspended, ingosteinke will not be able to comment or publish posts until their suspension is removed. Once unsuspended, ingosteinke will be able to comment and publish posts again. Once unpublished, all posts by ingosteinke will become hidden and only accessible to themselves. If ingosteinke is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Ingo Steinke. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag ingosteinke: ingosteinke consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging ingosteinke will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

Pure CSS parallax perspective beyond landscape images

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×