You’ve probably seen these gorgeous scrolling portfolios lately, featured on Codrops and several Awwwards portfolios. When I began to dig in to the methods for building one, I was disappointed to find most of them use Javascript or worse, WebGL. I thought back to the “news ticker” of yesteryear, knowing it was possible to recreate these effects in CSS alone, allowing for a leaner, better experience overall.
If you missed it, check out CSS Mastery: Animation @keyframes to get your mind thinking about how your elements should move. CSS transition and animation are almost the same, where animation can dig deeper by giving your element a defined timeline for when different style changes take effect or, in this case, to move something from one place to another.
Here, we combine 5 powerful CSS properties for an optimally responsive horizontal scrolling portfolio with text and hover effects: flexbox
@animation
loading
viewport units
clip-path
Disclaimer: you’re gonna see a little jerk when this loads due to Codepen
Breaking it down
The structure is pretty simple, allowing you to apply the styling shown here to other scenarios such as Elementor or Gutenberg blocks in WordPress without much fuss (you just need to get the class names on the right elements).
<div class="text-scroller">
<div class="scroller-left">
<div class="werd">Words go here</div>
</div>
<div class="scroller-right">
<div class="werd">Words go here</div>
</div>
</div>
Text Scroller
We use translate3d
in an animation @keyframe
to move the text and images from right to left with an animation we named “scroller”:
@keyframes scroller { 0% { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); visibility: visible; } 100% { -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } }
Next, the text-scroller
class just handles the styling and position of the scrolling text section. We also want to align the leading scroller to the left edge of the viewport so the copy can follow. This is what creates the “infinite” feel.
Positioning
.text-scroller{ position: fixed; display: inline-flex; width: 100%; overflow: hidden; height: 50vh; text-transform: uppercase; align-items: center; justify-content: center; } .scroller-left { left: 100%; }
Scroll Effect
And now for the effects. The important bits here are the white-space: nowrap
and animation properties. infinite and linear are the values that create the movement from right to left and the duration is how fast or slow it goes. The font size and number of words (or the length of your sentence) will also affect how fast the movement appears.
.werd { height: 4rem; padding-left: 1rem; line-height: 4rem; white-space: nowrap; -webkit-animation-iteration-count: infinite; animation-iteration-count: infinite; -webkit-animation-timing-function: linear; animation-timing-function: linear; -webkit-animation-name: scroller; animation-name: scroller; -webkit-animation-duration: 40s; animation-duration: 40s; font-size: 6vw; color: white; }
To allow the fonts to scale elegantly in the browser, I opted for the VW unit here versus rems.
Image Scroller
The image scroller uses much of the same structure and styles, with a couple variations to handle responsive scaling of the images, and a nice hover interaction.
<div class="image-scroller">
<div class="scroller-left">
<div class="look">
<img loading="lazy" src="..." />
</div>
</div>
<div class="scroller-right">
<div class="look">
<img loading="lazy" src="..." />
</div>
</div>
</div>
loading
Very often, webpages contain many images that contribute to data-usage and how fast a page can load. Most of those images are off-screen (non-critical), requiring user interaction (an example being scroll) in order to view them.
The loading
attribute on an <img>
element (or the loading
attribute on an <iframe>
) can be used to instruct the browser to defer loading of images/iframes that are off-screen until the user scrolls near them. This is a relatively new native lazy-load method that removes the need for JS. It probably doesn’t work in Edge/Internet Explorer.
.image-scroller{ position: fixed; display: flex; top: 24%; width: 100%; overflow: hidden; min-height: 80vh; align-items: center; justify-content: center; z-index: 444; }
I made this section a little taller so there is some space between the text and the landscape oriented images. I also added a z-index for those oddball browsers that have issues with clipping.
Scroll & Hover Effect
The styling of the image wrapper handles the scrolling in the exact same way as the text:
.image-scroller .look{ display: flex; -webkit-animation-iteration-count: infinite; animation-iteration-count: infinite; -webkit-animation-timing-function: linear; animation-timing-function: linear; -webkit-animation-name: scroller; animation-name: scroller; -webkit-animation-duration: 40s; animation-duration: 40s; justify-content: space-evenly; align-items: flex-end; }
The styling for the image itself handles three things: responsive scaling thanks to VW, setup for the hover interaction with clip-path and a cubic bezier transition which gives the image a smooth scaled-down effect without actually scaling the image (which tends to be wonky and pixelly).
.look > img{ width: 10vw; margin: 2em; filter: grayscale(0); -webkit-clip-path: inset(0 0 0 0); clip-path: inset(0 0 0 0); -webkit-transition: 1s cubic-bezier(.075,.82,.165,1); -moz-transition: 1s cubic-bezier(.075,.82,.165,1); transition: 1s cubic-bezier(.075,.82,.165,1); }
And now for the hover animation (using a variant of the bloom animation I teach in the @keyframe tutorial) and the cubic transition:
@keyframes bloom { 0% { filter: grayscale(0); } 40% { filter: grayscale(.5); } 100% { filter: grayscale(.8); } } .look img:hover{ animation: bloom ease-in-out .75s forwards; clip-path: inset(10px); }