Introduction
Hi all! I just want to say, before you delve in, that this really is a hard-and-fast explanation of how this is put together. Over the coming days I'm looking to revisit this with a video tutorial, including a more detailed explanation and a start-to-finish approach of how it's made. In the meantime though, I still wanted to share this in the hope that somebody may find it useful.
I was recently inspired by a portfolio website I discovered for a designer and developer named Joseph Berry. His website, aside from being both modern and beautiful, really got me interested in the idea of horizontal scrolling.
Horizontal scrolling has become somewhat of a trend on the web and, in my opinion, it is particularly good at building websites that want to convey a story - this is most likely due to its likeness to the way we scroll through a book. I also think it’s a nice surprise for a user when they expect to scroll down but, instead, the screen scrolls right.
Jumping through the code of Mr Berry’s site also got me thinking about something else. Could I do this in CSS alone? I’ve been really intrigued lately by the philosophy of progressive enhancement and seeing what can be done with the browser’s most reliable tools, HTML and CSS. Let’s see what we can come up with.
I knew off the bat there were a number things I probably wouldn’t be able to achieve with CSS alone, but that’s OK. Let’s focus on what we can do. If you’ve had a look at the website, you’ll see that there are two key visual effects - the horizontal scrolling, and the parallax effect. These are what we’ll be looking at here.
This is what we'll be making:
Let's Dive Right In
We’ll start with the HTML.
This is all we need in our HTML to achieve what we need. Let's break down what's happening.
The <main class="viewport">
element will be home to everything horizontal. Whilst this element isn't strictly necessary, as you could use the body
tag to the same result, using it will allow us to adjust the horizontal scrolling area down the line and allow us to more easily add other semantic HTML like a header
and footer
.
Following this we have our parallax-parent
element which, unsurprisingly, will be the container for anything we want to apply perspective to. There's a background-colors
element for our background, and some parallax-child-containers
which will house our pages/content.
The CSS
Our CSS isn't quite so straightforward, but it's also nothing mind-bending. The key to this entire setup is essentially one magical, almost limitless, CSS property - the transform
property.
I've started with some basic style-resets and also, having written this is SCSS, a couple of useful variables. We'll see shortly exactly how these will be used.
You can, of course, create this is vanilla CSS and simply hard-code these values.
The below code is all that's needed to style our viewport
element.
This simply ensures that our element takes up 100% of the available screen-space, and doesn't allow for any scrollbars to appear should the content exceed this size - which it will.
Our parallax-parent element is what will allow us to achieve the cool 3D effect found on many modern websites. You know the one - where one element moves a bit faster than the other, giving the illusion of them being closer and farther away respectively. We do this by applying perspective: 3px
to the element and setting it's perspective-origin
.
The default value of perspective-origin
is actually what you see above (50% 50% 0
) so there's no need to code this in. I put it there originally so I could play around with the values, so I've left it there in case you want to do the same.
The parallax-parent
also doubles up in one extremely important way, leading me to think later that maybe parallax-parent
wasn't the best naming for this element. It's this element which actually does the thing you came here for.
Check out line 8 of the code - transform: rotate(-90deg);
. This element is actually being rotated anti-clockwise so that it's top is it's left, it's left is it's bottom, it's...well, so on and so on.
We've also set the transform-origin: top left;
which means that the rotation will happen around the top left corner. Let me explain with a diagram.
This blue rectangle represents our parallax-parent
- in this case being displayed on something like a desktop screen. In the top left corner you'll notice a red circle. This is our transform-origin
.
So now, when we rotate this counter-clockwise by 90 degrees, or rotate(-90deg)
as CSS would have it, this is the point at which the rotation will occur.
And what does that look like? Well, on your screen, it will look like your element vanished! Why is that?
Well, our element has rotated its way out of the viewport and, therefor, can no longer be seen. This is where our use of top: $height
(or top: 100vh
for CSS) comes in to help us out to return the element back into view. You'll also note that our height
is set as $width
and our width
is set as $height
. This now adjusts our element to fit nicely back into the viewport like it was before, only this time rotated -90 degrees.
This has now become a very important point moving forward, as how we deal with top
, bottom
, left
, and right
in CSS with our absolute
positioned elements has been affected...and it can be somewhat confusing at first.
Lastly you'll notice that ::-webkit-scrollbar
has been used to hide the scrollbar in our element. An important note here is that this selector is not a web standard and will have different outcomes on different browsers, so be careful.
From the MDN docs:
::-webkit-scrollbar
is only available in Blink- and WebKit-based browsers (e.g., Chrome, Edge, Opera, Safari, all browsers on iOS, and others).
Our background-colors
element incorporates a simple CSS gradient to mimic a colour change as the user scrolls. In the example website I pointed to at the beginning of the post the developer uses JavaScript to change the colour dynamically, most likely with scroll event listeners. As we said we would be using plain S/CSS only, we'll stick with a gradient.
The element has been sized to cover $width * $pages
(or 100vw * 3 = 300vw
) and can be resized as necessary. Bare in mind, using this approach, if you want each page to have its own colour you will need to adjust the gradient accordingly.
The parallax-child-container
element uses much of the same styling with regards to sizing as its parent element, the parallax-parent
, with the width
and height
still being "reversed". Here I've used a SCSS function to iterate over the elements and apply a top
equivalent to the nth-of-type - 1 * $width
. Essentially this gives the first element a top: 0
, the second a top: 100vw
, the third a top: 200vw
, and so on. AGain, you could easily hard code this into the CSS as even with the SCSS we need to know how many elements (or $pages
) we have for the function to work.
Now, all of our "pages" take up a full-screen each, much like in the example portfolio or Joseph's.
The parallax-child
element is where we start to return back to "vertical viewing". Let's look at the different transforms we're using on this element.
To keep the element central within its parent, we apply a value 50%
to both top
and left
, and -50%
to both translateX
and translateY
. We also apply rotate(90deg)
to bring everything back to "normal", visually speaking. Again, the default values for transform-origin
have been left so there's no need to keep these should you be happy with them.
Finally, our z axis gets some love! By applying translateZ(-1px)
we see a shift "backwards" in our elements position. And, when we scroll, we notice that the element scrolls a little more slowly than before. It's very subtle, but noticeable if you focus on the gradient. If you're working against a plain background, you likely wouldn't apply this transform to these elements.
Lastly, we apply similar styling to the parallax-background
, only this time we push it "back" much further at -8px
.
You may have noticed that when we apply a value to translateZ
, we also apply a value to scale
. There is a useful write-up by Google explaining this in more detail and why this is done, but essentially it negates the resizing that takes place when we "push" or "pull" our elements backwards or forwards respectively. Imagine driving further away from a building, it get's smaller, right? The only way to make it seem the same size it was before, without driving back, is to scale it up. Voila!
Lastly, we size the background to cover the number of pages we have, in this case 3. Once we've applied some styling to the header nested in parallax-background
we're done!
I hope I was able to share something interesting with you here, and I’d really appreciate any feedback on either the content or the writing. I’d like to do this more often and making it worthwhile would be a massive bonus.
You can find the sourcecode for this project on Github.
Top comments (2)
Hey Martin
Thank you so much for this solution, this is pretty clever! Unfortunately it does not work on mobile. Have you an idea? It is pretty cool & clever to solve something like this with only css. Great work!
but can we use both horizontal and vertical scrolling in same page like if i am on service section then horizontal scrolling else vertical scrolling