Tag: Scroll

How I Added Scroll Snapping To My Twitter Timeline

CSS Scroll Snap allows websites to snap the web page or any other scroll container to a specific scroll position when the user performs a scrolling operation. This feature has been supported in all modern browsers for over two years, but many websites that could benefit from it are still not using it.

Scroll snapping is probably most associated with horizontal carousels (see Chris’s CSS-only approach) and particular web pages divided into full-screen slides. But why stop there? I believe that snapping can improve the scrolling experience on any web page that lays out items in a grid or feed.

For example, most shopping websites show products in a grid. Ideally, the user would like to jump between the grid rows with minimal effort. The user can press Space to scroll the page by roughly one screen (viewport height), but depending on the height of the grid rows, the scroll position will eventually get “out of sync” with the grid, and the user will have to re-adjust it manually.

If we add scroll snapping to this page, the user can consistently scroll to the next row with the Space key (pressing Shift + Space will scroll to the previous row). It’s pretty effortless.

I think that scroll snapping would be a welcome addition to this website. And it’s not even that complicated to implement. The CSS code that I used for this example is relatively simple:

html {   scroll-snap-type: y proximity; }  .product-item {   scroll-snap-align: start;   scroll-margin-top: 75px; /* height of web page’s sticky header */ }

You don’t have to wait if a website you visit regularly hasn’t yet added scroll snapping and you think it would improve your scrolling experience. You can add scroll snapping yourself — with user styles.

Adding user styles to websites

In the video above, you can see that I selected a user.css file in Safari’s advanced preferences. This file is a user style sheet. It contains CSS styles that I’ve written, stored in a local .css file, and added to Safari. These “user styles” are then applied to every web page I open in Safari.

Chrome and Firefox do not allow users to select a user style sheet. Firefox supported a similar feature called userContent.css in the past, but that feature was deprecated and disabled by default in 2019. I recommend the Stylus browser extension for these two browsers (and other Chromium-based browsers).

One significant advantage of Stylus is that it allows you to write user styles for specific websites and URLs. Safari’s user style sheet applies to all websites, but this can be worked around, e.g., by using the new :has() pseudo-class to create selectors that only match specific websites.

The Stylus extension has been reviewed by both Chrome and Firefox teams and received a badge that denotes high standards.

The CSS Cascading module defines a User Origin for styles the user adds. Safari’s user style sheet belongs to this origin, but the Stylus extension injects user styles to the Author Origin, where the website’s style sheets live. Specifically, Stylus inserts user styles directly to the page via a <style> element at the end of <html> which makes it the final style sheet on the page. Technically, this means styles added via Stylus are classified as author styles since they’re not in the User Origin, but I will continue to call them user styles because the user adds them.

However, it’s worth keeping this distinction in mind because it affects the cascade. When selector specificity is equal, a real user style is weaker than the page’s own style. This makes user styles an excellent fit for user defaults. Under the same conditions, a style added via Stylus is stronger than the page‘s style, so Stylus cannot as easily be used to define user defaults.

If we add !important to the mix, both real user styles and styles added via Stylus are stronger than the page’s styles. So when you want to impose your user styles on a website, it doesn’t matter if you use Safari’s “Style sheet” option or the Stylus extension. Your !important styles will win either way.

In the next section, I will use a set of !important user styles to enforce scroll snapping on the timeline page of Twitter’s website. My goal is to speed up the process of reading my Twitter timeline by avoiding awkward scroll positions where the topmost tweet is only partially on screen.

Scroll snap for Twitter’s timeline

After some experimentation, I’ve settled on the following CSS code. These styles work well in Firefox, but I’ve experienced some issues in Chrome and Safari. I will describe these issues in more detail later in the article, but for now, let’s focus on the behavior in Firefox.

html {   scroll-snap-type: y mandatory !important; }  /* tweets in the timeline are <article> elements */ article {   scroll-snap-align: start !important; }  /* un-stick the sticky header and make it “snappable” as well */ [aria-label="Home timeline"] > :first-child {   position: static !important;   scroll-snap-align: start !important; }  /* hide the “new Tweets available” floating toast notification */ [aria-label="New Tweets are available."] {   display: none !important; }

It is necessary to add !important to each declaration because all the user styles must win over the web page’s own styles for our custom scroll snapping implementation to work correctly. I wish that instead of repeatedly writing !important, I could just put my user styles in an “important layer,” but such a CSS feature does not exist (yet).

Watch the video below to see my scroll snap user styles in action. Notice how each press on the Space key scrolls the next set of tweets into view, and the first tweet of each set is aligned to the top edge of the viewport. This allows me to read my timeline more quickly. When I need to go back to the previous set of tweets, I can press Shift + Space.

What I like about this type of scroll snapping is that it allows me to predict how far the page will scroll whenever I press Space. Each scroll distance equals the combined heights of the visible tweets that are entirely on the screen. In other words, the partially visible tweet at the bottom of the screen will move to the top of the screen, which is precisely what I want.

I know in advance that pressing Space will scroll Dave’s tweet to the top of the screen.

To try out my scroll snap user styles on your own Twitter timeline, follow these steps:

  1. Install the Stylus extension with Firefox Add-ons or the Chrome Web Store.
  2. Navigate to your Twitter timeline at https://twitter.com/home.
  3. Click the Stylus icon in the browser’s toolbar and click “this URL” in the pop-up.
  4. Stylus will open a code editor in a new browser tab. Copy-paste my scroll snap user styles into the editor and press the Save button in the sidebar on the left. The styles will be applied to your Twitter timeline immediately (no need to reload the page).
  5. You can update the styles at any time. Click the Stylus icon and the Pencil icon to open the editor again.

Inability to override snapping

My implementation of scroll snapping for Twitter’s timeline has one major flaw. If a tweet is taller than the viewport, it is impossible to scroll the page to reveal the bottom part of that tweet (e.g., if you want to like or retweet that tweet) because the browser forcefully snaps the page to show the top of the tweet (or the top of the following tweet).

The severity of this problem depends on the user’s display. Viewing Twitter’s timeline on a large desktop monitor at a small page zoom factor, you may not encounter any tweets taller than the viewport.

I have asked the CSS Working Group if it would be possible to add a mechanism allowing the user to override the browser’s mandatory scroll snapping. I should probably mention that this problem could, at least in theory, be resolved by switching from mandatory to proximity snapping. I’ve tested proximity snapping in Chrome and Firefox, and I found it inconsistent and confusing. The browser would often snap when I didn’t expect it to, and vice versa. Maybe Twitter’s code is interfering with the proximity algorithm, the browsers are still a bit buggy, or perhaps I’m just “scrolling it wrong,” if that’s even possible. I don’t know.

But the main reason why I went with mandatory snapping is that I wanted to avoid situations where the topmost tweet is only partially on screen after a scroll. The type of fast-scrolling between sets of tweets that I’ve shown in the video above is only possible with mandatory snapping.

If you, like me, prefer mandatory snapping, I can suggest the following two workarounds for the “tall tweet” problem:

  • You can open the tweet on its own page and return to the timeline afterward.
  • If you only want to click the Like or Retweet buttons, you can Shift-click the tweet to select it and then press L to like it, or T followed by Enter to retweet it.

Issues in Chrome and Safari

My scroll snap user styles produce noticeably different scroll snapping behaviors in Chrome, Safari, and Firefox. Those differences are in part since the exact implementation of the snapping mechanism is left up to the browser:

The CSS Scroll Snap Module intentionally does not specify nor mandate any precise animations or physics used to enforce snap positions; this is left up to the user agent.

The current version of Safari has a bug that prevents scroll snapping from working correctly on the Twitter timeline. I have reported this bug.

In Chrome, I have encountered the following problems:

  • The scrolling operations animate inconsistently. Sometimes the animation is slow, sometimes it’s instant, and sometimes it starts slow but is then cut short. I found this irritating.
  • The scrolling operations animate too slowly in general. I performed a test in Chrome and Firefox (20 Space presses), and it took me 70% more time to cover the same distance on my Twitter timeline in Chrome than in Firefox (18.5 seconds in Chrome vs. 11 seconds in Firefox).
  • When I scroll using my laptop’s trackpad, the page flickers a lot. When I attempt to scroll fast by holding down the Space key, the page scrolls very slowly and oscillates. I suspect that both issues are caused by the same algorithm. It seems that Chrome re-snaps at a very high rate in these cases. I have reported this bug.

These browser bugs and differences between browsers can be a problem for websites considering implementing scroll snapping. For example, a web developer might hold back because they don’t like how scroll snapping behaves in one particular browser. Browsers can mitigate this problem by becoming more interoperable. In fact, Scroll Snap is one of the areas of focus of the cross-browser Interop 2022 effort.

Another way the situation could be improved is by introducing new CSS properties that would make scroll snapping more configurable. This could include the duration of the snapping animation, the length of the proximity threshold for snapping, and a mechanism to override mandatory snapping.

To snap or not to snap?

I’ve been using my scroll snap user styles on Twitter’s timeline for a couple of weeks, and I don’t want to go back. The ability to quickly flip through my feed with only the Space key is just on another level.

However, I consider this an advanced feature that probably isn’t for everyone. There’s a reason why I’ve enabled it only on the timeline (/home path) and nowhere else on Twitter’s website. Snapping is a significant change in how the page scrolls, and it takes some time to get used to. It can work great for a specific use case, but it can also get in the way and frustrate the user.

Websites with feeds should therefore consider offering scroll snapping only as an optional feature, after careful consideration and plenty of testing in different browsers and with different input methods (mouse, keyboard, trackpad, touch screen, etc.).

Before you go…

Finally, I highly recommend installing and trying out the Stylus browser extension. Web developers (or anyone who knows CSS) have the power to style any website in their browser. You can apply minor improvements and fixes to your favorite websites. I mostly use it to hide page elements that I find annoying, such as sticky headers, video pop-ups, and vote counts.

But more importantly, Stylus allows you to quickly test new CSS features on any website and report browser bugs, if necessary. By doing this, you can help make the web platform a little better.

How I Added Scroll Snapping To My Twitter Timeline originally published on CSS-Tricks. You should get the newsletter.


, , , ,

Scroll Shadows? Pure CSS Parallax? Game Back On.

Chris calls scroll shadows one his favorite CSS-Tricks of all time. Lea Verou popularized the pure CSS approach using four layered background gradients with some clever background-attachment magic. The result is a slick scrolling interaction that gives users a hint that additional content is available in a scrollable container.

Just one problem: it broke in Safari iOS 13. One day it was all good. The next, not so much. And that wasn’t the only thing affected. Keith Clark’s CSS-only parallax effect also stopped working right about then.

Well, reader Ronald wrote in to say that all is working once again! In fact, I’m writing this on my iPad (Safari 15.5) right now and Chris’s demo is looking sharp as ever. So is Keith’s original demo.

So, what broke it? We still don’t know. But the Safari 13 release notes offer clues:

  • Added support for one-finger accelerated scrolling to all frames and overflow:scroll elements eliminating the need to set-webkit-overflow-scrolling: touch.
  • Changed the default behavior on iPad for wide web pages with responsive meta-tags that require horizontal scrolling. Pages are scaled to prevent horizontal scrolling and any text is resized to preserve legibility.

When was it fixed and what fixed it? Well, on the scroll shadow side, the Safari 15.4 included some work on background-attachment: local that may have done the trick. On the parallax side, Safari 14.1 added support for individual transform properties… so maybe that?

Scroll Shadows? Pure CSS Parallax? Game Back On. originally published on CSS-Tricks. You should get the newsletter.


, , , , ,

CSS Scroll Snap Slide Deck That Supports Live Coding

Virtual conferences have changed the game in terms of how a presenter is able to deliver content to an audience. At a live event it’s likely you just have your laptop, but at home, you may have multiple monitors so that you can move around windows and make off-screen changes when delivering live coding demos. However, as some events go back to in-person, you may be in a similar boat as me wondering how to bring an equivalent experience to a live venue.

With a bit of creativity using native web functionality and modern CSS, like CSS scroll snap, we’ll be building a no-JavaScript slide deck that allows live editing of CSS demos. The final deck will be responsive and shareable, thanks to living inside of a CodePen.

To make this slide deck, we’ll learn about:

  • CSS scroll snap, counters, and grid layout
  • The contenteditable attribute
  • Using custom properties and HSL for theming
  • Gradient text
  • Styling the <style> element

Slide templates

When making a slide deck of a bunch of different slides, it’s likely that you’ll need different types of slides. So we’ll create these three essential templates:

  • Text: open for any text you need to include
  • Title: emphasizing a headline to break up sections of content
  • Demo: split layout with a code block and the preview
Screenshot of the text slide containing a heading, byline, and Twitter handle. The text is dark blue and the background has a light blue. There is the number one in white inside a bright blue circle located in the bottom-left corner of the slide indicating the page title.
Text presentation slide

A slide with the text Topic 1 in dark blue and a soft linear gradient that goes from a super light blue to a brighter blue, going from left to right.
Title presentation slide

Slide deck screenshot showing the split view with live code on the left and the output on the right. The page number of the slide is shown in the bottom-left corner and includes the word CSS after the page number.
Demo presentation slide

HTML templates

Let’s start creating our HTML. We’ll use an ordered list with the ID of slides and go ahead and populate a text and title slide.

Each slide is one of the list elements with the class of slide, as well as a modifier class to indicate the template type. For these text-based slides, we’ve nested a <div> with the class of content and then added a bit of boilerplate text.

<ol id="slides">   <li class="slide slide--text">     <div class="content">       <h1>Presentation Title</h1>       <p>Presented by Your Name</p>       <p><a target="_blank" href="<https://twitter.com/5t3ph>">@5t3ph</a></p>     </div>   </li>   <li class="slide slide--title">     <div class="content">       <h2>Topic 1</h2>     </div>   </li> </ol>

We’re using target="_blank" on the link due to CodePen using iframes for the preview, so it’s necessary to “escape” the iframe and load the link.

Base styles

Next, we’ll begin to add some styles. If you are using CodePen, these styles assume you’re not loading one of the resets. Our reset wipes out margin and ensures the <body> element takes up the total available height, which is all we really need here. And, we’ll make a basic font stack update.

* {   margin: 0;   box-sizing: border-box; }  body {   min-height: 100vh;   font-family: system-ui, sans-serif;   font-size: 1.5rem; }

Next, we’ll define that all our major layout elements will use a CSS grid, remove list styling from #slides, and make each slide take up the size of the viewport. Finally, we’ll use the place-content shorthand to center the slide--text and slide--title slide content.

body, #slides, .slide {   display: grid; }  #slides {   list-style: none;   padding: 0;   margin: 0; }  .slide {   width: 100vw;   height: 100vh; }  .slide--text, .slide--title {   place-content: center; }

Then, we’ll add some lightweight text styles. Since this is intended to be a presentation with one big point being made at a time, as opposed to an article-like format, we’ll bump the base font-size to 2rem. Be sure to adjust this value as you test out your final slides in full screen. You may decide it feels too small for your content versus your presentation viewport size.

h1, h2 {   line-height: 1.1; }  a {   color: inherit; }  .content {   padding: 2rem;   font-size: 2rem;   line-height: 1.5; }  .content * + * {   margin-top: 0.5em; }  .slide--text .content {   max-width: 40ch; }

At this point, we have some large text centered within a container the size of the viewport. Let’s add a touch of color by creating a simple theme system.

We’ll be using the hsl color space for the theme while setting a custom property of --theme-hue and --theme-saturation. The hue value of 230 corresponds to a blue. For ease of use, we’ll then combine those into the --theme-hs value to drop into instances of hsl.

:root {   --theme-hue: 230;   --theme-saturation: 85%;   --theme-hs: var(--theme-hue), var(--theme-saturation); }

We can adjust the lightness values for backgrounds and text. The slides will feel cohesive since they will be tints of that base hue.

Back in our main <body> style, we can apply this idea to create a very light version of the color for a background, and a dark version for the text.

body {   /* ... existing styles */   background-color: hsl(var(--theme-hs), 95%);   color: hsl(var(--theme-hs), 25%); }
Screenshot of a CSS scroll snap slide  with the presentation title, a byline, and a Twitter handle. The text is dark blue and the background is a light blue.

Let’s also give .slide--title a little bit of extra pizazz by adding a subtle gradient background.

.slide--title {   background-image:      linear-gradient(125deg,        hsl(var(--theme-hs), 95%),        hsl(var(--theme-hs), 75%)     ); }
A slide with the text Topic 1 in dark blue and a soft linear gradient that goes from a super light blue to a brighter blue, going from left to right.

Demo slide template

Our demo slide breaks the mold so far and requires two main elements:

  • a .style container around an inline <style> element with actual written styles that you intend to both be visible on screen and apply to the demo
  • a .demo container to hold the demo preview with whatever markup is appropriate for that

If you’re using CodePen to create this deck, you’ll want to update the “Behavior” setting to turn off “Format on Save.” This is because we don’t want extra tabs/spaces prior to the styles block. Exactly why will become clear in a moment.

Screenshot of a CodePen's HTMLand CSS code panels. The settings menu for the HTML panel is open and highlighting the first item, which is Format HTML.

Here’s our demo slide content:

<li class="slide slide--demo">   <div class="style">   <style contenteditable="true">  .modern-container {   --container-width: 40ch;    width: min(     var(--container-width), 100% - 3rem   );   margin-inline: auto; }   </style>   </div>   <div class="demo">     <div class="modern-container">       <div class="box">container</div>     </div>   </div> </li>

Note that extra contenteditable="true" attribute on the <style> block . This is a native HTML feature that allows you to mark any element as editable. It is not a replacement for form inputs and textareas and typically requires JavaScript for more full-featured functionality. But for our purposes, it’s the magic that enables “live” coding. Ultimately, we’ll be able to make changes to the content in here and the style changes will apply immediately. Pretty fancy, hold tight.

However, if you view this so far, you won’t see the style block displayed. You will see the outcome of the .modern-container demo styles are being applied, though.

Another relevant note here is that HTML5 included validating a <style> block anywhere; not just in the <head>.

What we’re going to do next will feel strange, but we can actually use display properties on <style> to make it visible. We’ve placed it within another container to use a little extra positioning for it and make it a resizable area. Then, we’ve set the <style> element itself to display: block and applied properties to give it a code editor look and feel.

.style {   display: grid;   align-items: center;   background-color: hsl(var(--theme-hs), 5%);   padding-inline: max(5vw, 2rem) 3rem;   font-size: 1.35rem;   overflow-y: hidden;   resize: horizontal; }  style {   display: block;   outline: none;   font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;   color: hsl(var(--theme-hs), 85%);   background: none;   white-space: pre;   line-height: 1.65;   tab-size: 2;   hyphens: none; }

Then, we need to create the .slide--demo rule and use CSS grid to display the styles and demo, side-by-side. As a reminder, we’ve already set up the base .slide class to use grid, so now we’ll create a rule for grid-template-columns just for this template.

.slide--demo {   grid-template-columns: fit-content(85ch) 1fr; }

If you’re unfamiliar with the grid function fit-content(), it allows an element to use its intrinsic width up until the maximum value defined in the function. So, this rule says the style block can grow to a maximum of 85ch wide. When your <style> content is narrow, the column will only be as wide as it needs to be. This is really nice visually as it won’t create extra horizontal space while still ultimately capping the allowed width.

To round out this template, we’ll add some padding for the .demo. You may have also noticed that extra class within the demo of .box. This is a convention I like to use for demos to provide a visual of element boundaries when the size and position of something are important.

.demo {   padding: 2rem; }  .box {   background-color: hsl(var(--theme-hs), 85%);   border: 2px dashed;   border-radius: .5em;   padding: 1rem;   font-size: 1.35rem;   text-align: center; }

Here’s the result of our code template:

Screenshot of a slide that's split in half vertically, the left side with a almost black dark blue background and code that is a lighter blue in a mono font. The right side has a light blue background and an element at the top that says container, with a dashed border and slightly darker blue background.

Live-editing functionality

Interacting with the displayed styles will actually update the preview! Additionally, since we created the .style container as a resizable area, you can grab the resize handle in the lower right to grow and shrink the preview area as needed.

The one caveat for our live-editing ability is that browsers treat it differently.

  • Firefox: This provides the best result as it allows both changing the loaded styles and full functionality of adding new properties and even new rules.
  • Chromium and Safari: These allow changing values in loaded styles, but not adding new properties or new rules.

As a presenter, you’ll likely want to use Firefox. As for viewers utilizing the presentation link, they’ll still be able to get the intention of your slides and shouldn’t have issues with the display (unless their browser doesn’t support your demoed code). But outside of Firefox, they may be unable to manipulate the demos as fully as you may show in your presentation.

You may want to “Fork” your finished presentation pen and actually remove the editable behavior on <style> blocks and instead display final versions of your demos styles, as applicable.

Reminder: styles you include in demos can potentially affect slide layout and other demos! You may want to scope demo styles under a slide-specific class to prevent unintended style changes across your deck.

Code highlighting

While we won’t be able to achieve full syntax highlighting without JavaScript, we can create a method to highlight certain parts of the code block for emphasis.

To do this, we’ll pair up linear-gradient with the -webkit properties that enable using an element’s background as the text effect. Then, using custom properties, we can define how many “lines” of the style block to highlight.

First, we’ll place the required -webkit properties directly on the <style> element. This will cause the visible text to disappear, but we’ll make it visible in a moment by adding a background. Although these are -webkit prefixed, they are supported cross-browser.

style {   /* ...existing styles */   -webkit-text-fill-color: transparent;   -webkit-background-clip: text; }

The highlighting effect will work by creating a linear-gradient with two colors where the lighter color shows through as the text color for the lines to highlight. As a default, we’ll bookend the highlight with a darker color such that it appears that the first property is highlighted.

Here’s a preview of the initial effect:

An up-close screenshot of the live code panel of the slide, with the second line of code a lighter blue than the rest, indicating that it is emphasized.

To create this effect, we need to work out how to calculate the height of the highlight color. In our <style> element’s rules, we’ve already set the line-height to 1.65, which corresponds to a total computed line height of 1.65em. So, you may think that we multiply that by the number of lines and call it a day.

However, due to the visible style block being rendered using white-space: pre to preserve line breaks, there’s technically a sneaky invisible line before the first line of text. This is created from formatting the <style> tag on an actual line prior to the first line of CSS code. This is also why I noted that preventing auto-formatting in CodePen is important — otherwise, you would also have extra left padding.

With these caveats in mind, we’ll set up three custom properties to help compute the values we need and add them to the beginning of our .style ruleset. The final --lines height value first takes into account that invisible line and the selector.

style {   --line-height: 1.65em;   --highlight-start: calc(2 * var(--line-height));   --lines: calc(var(--highlight-start) + var(--num-lines, 1) * var(--line-height)); }

Now we can apply the values to create the linear-gradient. To create the sharp transitions we need for this effect, we ensure the gradient stops from one color to the next match.

style {   background-image: linear-gradient(     hsl(var(--theme-hs), 75%) 0 var(--highlight-start),     hsl(var(--theme-hs), 90%) var(--highlight-start) var(--lines),     hsl(var(--theme-hs), 75%) var(--lines) 100%   ); }

To help visualize what’s happening, I’ve commented out the -webkit lines to reveal the gradient being created.

A close-up screenshot of the live code example, but with a bright blue background to reveal the near-white gradient that highlights the second line of code.

Within our --lines calculation, we also included a --num-lines property. This will let you adjust the number of lines to highlight per demo via an inline style. This example adjusts the highlight to three lines:

<style contenteditable="true" style="--num-lines: 3">

We can also pass a recalculated --highlight-start to change the initial line highlighted:

<style contenteditable="true" style="--num-lines: 3; --highlight-start: calc(4 * var(--line-height))">

Let’s look at the outcome of the previous adjustment:

Showing the live code example with lines 3 through 6 highlighted in a lighter blue than the rest of the code.

Now, if you add or remove lines during your presentation, the highlighting will not adjust. But it’s still nice as a tool to help direct your viewers’ attention.

There are two utility classes we’ll add for highlighting the rule only or removing highlighting altogether. To use, apply directly to the <style> element for the demo.

.highlight--rule-only {   --highlight-start: calc(1 * var(--line-height)) }  .highlight--none {   background-image: none;   background-color: currentColor; }

Slide motion with CSS scroll snap

Alright, we have some nice-looking initial slides. But it’s not quite feeling like a slide deck yet. We’ll resolve that in two parts:

  1. Reflow the slides horizontally
  2. Use CSS scroll snap to enforce scrolling one slide at a time

Our initial styles already defined the #slides ordered list as a grid container. To accomplish a horizontal layout, we need to add one extra property since the .slides have already included dimensions to fill the viewport.

#slides {   /* ...existing styles */   grid-auto-flow: column; }

For CSS scroll snap to work, we need to define which axis allows overflow, so for horizontal scrolling, that’s x:

#slides {   overflow-x: auto; }

The final property we need for scroll snapping for the #slides container is to define scroll-snap-type. This is a shorthand where we select the x axis, and the mandatory behavior, which means initiating scrolling should always trigger snapping to the next element.

#slides {   scroll-snap-type: x mandatory; }

If you try it at this point, you won’t experience the scroll snapping behavior yet because we have two properties to add to the child .slide elements. Use of scroll-snap-align tells the browser where to “snap” to, and setting scroll-snap-stopto always prevents scrolling past one of the child elements.

.slide {   /* ...existing styles */   scroll-snap-align: center;   scroll-snap-stop: always; }

The scroll snapping behavior should work either by scrolling across your slide or using left and right arrow keys.

There are more properties that can be set for CSS scroll snap, you can review the MDN docs to learn what all is available. CSS scroll snap also has a bit different behavior cross-browser, and across input types, like touch versus mouse, or touchpad versus mouse wheel, or via scrollbar arrows. For our presentation, if you find that scrolling isn’t very smooth or “snapping” then try using arrow keys instead.

Currently, there isn’t a way to customize the CSS scroll snap sliding animation easing or speed. Perhaps that is important to you for your presentation, and you don’t need the other features we’ve developed for modifying the code samples. In that case, you may want to choose a “real” presentation application.

CSS scroll snap is very cool but also has some caveats to be aware of if you’re thinking of using it beyond our slide deck context. Check out another scroll snapping demo and more information on SmolCSS.dev.

Slide numbers

An optional feature is adding visible slide numbers. Using a CSS counter, we can get the current slide number and display it however we’d like as the value of a pseudo-element. And using data attributes, we can even append the current topic.

The first step is giving our counter a name, which is done via the counter-reset property. This is placed on the element that contains items to be counted, so we’ll add it to #slides.

#slides {   counter-reset: slides; }

Then, on the elements to be counted (.slide), we add the counter-increment property and callback to the name of the counter we defined.

.slide {   counter-increment: slides; }

To access the current count, we’ll set up a pseudo element. Within the content property, the counter() function is available. This function accepts the name of our counter and returns the current number.

.slide::before {   content: counter(slides); }

The number is now appearing but not where we want it. Because our slide content is variable, we’ll use classic absolute positioning to place the slide number in the bottom-left corner. And we’ll add some visual styles to make it enclosed in a nice little circle.

.slide::before {   content: counter(slides);   position: absolute;   left: 1rem;   bottom: 1rem;   width: 1.65em;   height: 1.65em;   display: grid;   place-content: center;   border-radius: 50%;   font-size: 1.25rem;   color: hsl(var(--theme-hs), 95%);   background-color: hsl(var(--theme-hs), 55%); }
Screenshot of the text slide containing a heading, byline, and Twitter handle. The text is dark blue and the background has a light blue. There is the number one in white inside a bright blue circle located in the bottom-left corner of the slide indicating the page title.

We can enhance our slide numbers by grabbing the value of a data attribute to also append a short topic title. This means first adding an attribute to each <li> element where we want this to happen. We’ll add data-topic to the <li> for the title and code demo slides. The value can be whatever you want, but shorter strings will display best.

<li class="slide slide--title" data-topic="CSS">

We’ll use the attribute as a selector to change the pseudo element. We can get the value by using the attr() function, which we’ll concatenate with the slide number and add a colon for a separator. Since the element was previously a circle, there are a few other properties to update.

[data-topic]::before {   content: counter(slides) ": " attr(data-topic);   padding: 0.25em 0.4em;   width: auto;   border-radius: 0.5rem; }

With that added, here’s the code demo slide showing the added topic of “CSS”:

Slide deck screenshot showing the split view with live code on the left and the output on the right. The page number of the slide is shown in the bottom-left corner and includes the word CSS after the page number.

Small viewport styles

Our slides are already somewhat responsive, but eventually, there will be problems with horizontal scrolling on smaller viewports. My suggestion is to remove the CSS scroll snap and let the slides flow vertically.

To accomplish this will just be a handful of updates, including adding a border to help separate slide content.

First, we’ll move the CSS scroll snap related properties for #slides into a media query to only apply above 120ch.

@media screen and (min-width: 120ch) {   #slides {     grid-auto-flow: column;     overflow-x: auto;      scroll-snap-type: x mandatory;   } }

Next, we’ll move the CSS scroll snap and dimension properties for .slide into this media query as well.

@media screen and (min-width: 120ch) {   .slide {     width: 100vw;     height: 100vh;     scroll-snap-align: center;     scroll-snap-stop: always;   } }

To stack the demo content, we’ll move our entire rule for .slide--demo into this media query:

@media screen and (min-width: 120ch) {   .slide--demo {     grid-template-columns: fit-content(85ch) 1fr;   } }

Now everything is stacked, but we want to bring back a minimum height for each slide and then add the border I mentioned earlier:

@media (max-width: 120ch) {   .slide {     min-height: 80vh;   }    .slide + .slide {     border-top: 1px dashed;   } }

Your content also might be at risk of overflow on smaller viewports, so we’ll do a couple of adjustments for .content to try to prevent that We’ll add a default width that will be used on small viewports, and move our previous max-width constraint into the media query. Also shown is a quick method updating our <h1> to use fluid type.

h1 {   font-size: clamp(2rem, 8vw + 1rem, 3.25rem); }  .content {   /* remove max-width rule from here */   width: calc(100vw - 2rem); }  @media screen and (min-width: 120ch) {   .content {     width: unset;     max-width: 45ch;   } }

Additionally, I found it helps to reposition the slide counter. For that, we’ll adjust the initial styles to place it in the top-left, then move it back to the bottom in our media query.

.slide {   /* adjust default here, removing the old "bottom" value */   top: 0.25rem;   left: 0.25rem; }  @media (min-width: 120ch) {   .slide::before {     top: auto;     bottom: 1rem;     left: 1rem;   } }

Final slide deck

The embed will likely be showing the stacked small viewport version, so be sure to open the full version in CodePen, or jump to the live view. As a reminder, the editing ability works best in Firefox.

If you’re interested in seeing a fully finished deck in action, I used this technique to present my modern CSS toolkit.

CSS Scroll Snap Slide Deck That Supports Live Coding originally published on CSS-Tricks. You should get the newsletter and become a supporter.


, , , , , ,

Scroll Shadows With JavaScript

Scroll shadows are when you can see a little inset shadow on elements if (and only if) you can scroll in that direction. It’s just good UX. You can actually pull it off in CSS, which I think is amazing and one of the great CSS tricks. Except… it just doesn’t work on iOS Safari. It used to work, and then it broke in iOS 13, along with some other useful CSS things, with no explanation why and has never been fixed.

So, now, if you really want scroll shadows (I think they are extra useful on mobile browsers anyway), it’s probably best to reach for JavaScript.

Here’s a pure CSS example so you can see it work in all browsers except iOS Safari. Screenshots:

I’m bringing this up now because I see Jonnie Hallman is blogging about tit again. He mentioned it as an awesome little detail back in May. There are certain interfaces where scroll shadows really extra make sense.

Taking a step back, I thought about the solution that currently worked, using scroll events. If the scroll area has scrolled, show the top and left shadows. If the scroll area isn’t all the way scrolled, show the bottom and right shadows. With this in mind, I tried the simplest, most straight-forward, and least clever approach by putting empty divs at the top, right, bottom, and left of the scroll areas. I called these “edges”, and I observed them using the Intersection Observer API. If any of the edges were not intersecting with the scroll area, I could assume that the edge in question had been scrolled, and I could show the shadow for that edge. Then, once the edge is intersecting, I could assume that the scroll area has reached the edge of the scroll, so I could hide that shadow.

Clever clever. No live demo, unfortunately, but read the post for a few extra details on the implementation.

Other JavaScript-powered examples

I do think if you’re going to do this you should go the IntersectionObserver route though. Would love to see someone port the best of these ideas all together (wink wink).

The post Scroll Shadows With JavaScript appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, ,

2021 Scroll Survey Report

Here’s a common thought and question: how do browsers prioritize what they work on? We get little glimpses of it sometimes. We’re told to “star issues” in bug trackers to signal interest. We’re told to get involved in GitHub threads for spec issues. We’re told they do read the blog posts. And, sometimes, we get to see the results of surveys. Chrome ran a survey about scrolling on the web back in April and has published the results with an accompanying a blog post.

“Scrolling” is a big landscape:

From our research, these difficulties come from the multitude of use cases for scroll. When we talk about scrolling, that might include:

According to the results, dang near half of developers are dissatisfied with scrolling on the web, so this is a metric Google devs want to change and they will prioritize it.

To add to the list above, I think even smooth scrolling is a little frustrating in how you can’t control the speed or other behaviors of it. For example, you can’t say “smooth scroll an on-page jump-down link, but don’t smooth scroll a find-on-page jump.”

And that’s not to mention scroll snapping, which is another whole thing with the occasional bug. Speaking of which, Dave had an idea on the show the other day that was pretty interesting. Now that scroll snapping is largely supported, even on desktop, and feels pretty smooth for the most part, should we start using it more liberally, like on whole page sections? Maybe even like…

/* Reset stylesheet */ main, section, article, footer {   scroll-snap-align: start; }

I’ve certainly seen scroll snapping in more places. Like this example from Scott Jehl where he was playing with scroll snapping on fixed table headers and columns. It’s a very nice touch:

Direct Link to ArticlePermalink

The post 2021 Scroll Survey Report appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , ,

Yet Another Mobile Context Menu With No Indication it Can Scroll

Remember Tyler Hall’s personal story of a UX moment where the popup sharing context menu on iOS had no visible indication that the content inside was scrollable? The thing his mom wanted to do seemed impossible iOS isn’t alone here — Terence Eden documents essentially the same problem on Android:

I tried sharing a website using Google Chrome for Android. I hit the share button, and a panel popped-up from the bottom of the screen.

Hmmm. It didn’t have the share destination that I wanted. It was early in the morning – when I’m not at my cognitive best – and I was stumped. There is nothing on this screen – other than the icons – to tell me how I can interact with it. There’s no scrollbar, no handle, no “more” icon, nothing.

I would think even just fairly subtle “scroll shadows” would go a long way in both cases, but some serious user testing should be in order here.

Android menu with no indication of scrolling potential.

iOS menu with no indication of scrolling potential.

The post Yet Another Mobile Context Menu With No Indication it Can Scroll appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Practical Use Cases for Scroll-Linked Animations in CSS with Scroll Timelines

The Scroll-Linked Animations specification is an upcoming and experimental addition to CSS. Using the @scroll-timeline at-rule and animation-timeline property this specification provides you can control the time position of regular CSS Animations by scrolling.

In this post, we take a look at some practical use cases where scroll-linked animations come in handy, replacing a typical JavaScript approach.

👨‍🔬 The CSS features described in this post are still experimental and not finalized at all. The are not supported by any browser at the time of writing, except for Chromium ≥ 89 with the #experimental-web-platform-features flag enabled.

CSS Scroll-Linked Animations, a quick primer

With CSS Scroll-Linked Animations, you can drive a CSS animation by scroll: as you scroll up or down inside a scroll container, the linked CSS animation will advance or rewind. Best of all is that this is all running off main thread, on the compositor.

You need three things to implement a basic scroll-linked animation:

  1. a CSS animation
  2. a scroll timeline
  3. a link between both

CSS animation

This is a regular CSS Animation like we already know:

@keyframes adjust-progressbar {   from {     transform: scaleX(0);   }   to {     transform: scaleX(1);   } }

As you normally do, attach it to an element using the animation property:

#progressbar {   animation: 1s linear forwards adjust-progressbar; }

Scroll timeline

The scroll timeline allows us to map the scroll distance to the animation progress. In CSS, we describe this with the CSS @scroll-timeline at-rule.

@scroll-timeline scroll-in-document-timeline {   source: auto;   orientation: vertical;   scroll-offsets: 0%, 100%; }

This at-rule consists of descriptors, including:

  1. The source describes the scrollable element whose scrolling triggers the activation and drives the progress of the timeline. By default, this is the entire document.
  2. The orientation determines the scrolling direction that should trigger the animation. By default, this is vertical.
  3. The scroll-offsets property is an array of key points that describe the range in which the animation should be active. It can be absolute values (e.g. percentages and lengths) or element-based.

A previous version of the specification required you to also set a time-range descriptor. This descriptor has been removed and will automatically take over the animation-duration from the linked animation. You may still see traces of it in the demos, but you can safely ignore it.

To associate our @scroll-timeline with our CSS animation, we use the new animation-timeline CSS property, and have it refer to the timeline’s name.

#progressbar {   animation: 1s linear forwards adjust-progressbar;   animation-timeline: scroll-in-document-timeline; /* 👈 THIS! */ }

With that set up the adjust-progressbar animation won’t run automatically on page load, but will only advance as we scroll down the page.

For a more in-depth introduction to @scroll-timeline please refer to Part 1 and Part 2 of my series on the future of scroll-linked animations.

The first post looks at each descriptor/property in more detail, explaining them with an example to go along with them, before covering many more interesting demos.

The second post digs even deeper, looking into Element-Based Offsets, which allow us to drive an animation as an element appears into and disappears from the scrollport as we scroll.

An example of what you can achieve with CSS Scroll-Linked Animations using Element-Based Offsets.

Practical use cases

Apart from the progress par demo above, there are a few more use cases or scenarios where scroll-linked animations can replace a solution typically implemented using JavaScript.

  1. parallax header
  2. image reveal/hide
  3. typing animation
  4. carousel indicators
  5. scrollspy

Parallax header

A typical use case for Scroll-Linked Animations is a parallax effect, where several sections of a page seem to have a different scrolling speed. There’s a way to create these type of effects using only CSS, but that requires mind-bending transform hacks involving translate-z() and scale().

Inspired upon the Firewatch Header—which uses the mentioned transform hack—I created this version that uses a CSS scroll timeline:

Compared to the original demo:

  • The markup was kept, except for that extra .parallax__cover that’s no longer needed.
  • The <body> was given a min-height to create some scroll-estate.
  • The positioning of the .parallax element and its .parallax_layer child elements was tweaked.
  • The transform/perspective-hack was replaced with a scroll timeline.

Each different layer uses the same scroll timeline: scroll over a distance of 100vh.

@scroll-timeline scroll-for-100vh {   time-range: 1s;   scroll-offsets: 0, 100vh; }  .parallax__layer {   animation: 1s parallax linear;   animation-timeline: scroll-for-100vh; }

What’s different between layers is the distance that they move as we scroll down:

  • The foremost layer should stay in place, eg. move for 0vh.
  • The last layer should should move the fastest, e.g. 100vh.
  • All layers in between are interpolated.
@keyframes parallax {   to {     transform: translateY(var(--offset));   } }  .parallax__layer__0 {   --offset: 100vh; }  .parallax__layer__1 {   --offset: 83vh; }  .parallax__layer__2 {   --offset: 67vh; }  .parallax__layer__3 {   --offset: 50vh; }  .parallax__layer__4 {   --offset: 34vh; }  .parallax__layer__5 {   --offset: 17vh; }  .parallax__layer__6 {   --offset: 0vh; }

As the foremost layers move over a greater distance, they appear to move faster than the lower layers, achieving the parallax effect.

Image reveal/hide

Another great use-case for scroll linked animations is an image reveal: an image slides into view as it appears.

By default, the image is given an opacity of 0 and is masked using a clip-path:

#revealing-image {   opacity: 0;   clip-path: inset(45% 20% 45% 20%); }

In the end-state we want the image to be fully visible, so we sent the end-frame of our animation to reflect that:

@keyframes reveal {   to {     clip-path: inset(0% 0% 0% 0%);     opacity: 1;   } }

By using element-based offsets as our scroll timeline offsets, we can have it so thar the image begins to appear only when the image itself slides into view.

@scroll-timeline revealing-image-timeline {   scroll-offsets:     selector(#revealing-image) end 0.5,     selector(#revealing-image) end 1   ; }  #revealing-image {   animation: reveal 1s linear forwards;   animation-timeline: revealing-image-timeline; }

😵 Can’t follow with those element-based offsets? This visualization/tool has got you covered.

Typing animation

As CSS scroll timelines can be linked to any existing CSS animation, you can basically take any CSS Animation demo and transform it. Take this typing animation for example:

With the addition of a scroll timeline and the animation-timeline property, it can be adjusted to “type on scroll”:

Note that to create some scroll-estate the <body>was also given a height of 300vh.

Using a different animation, the code above can easily be adjusted to create a zoom on scroll effect:

I can see these two working great for article intros.

One of the components of a carousel (aka slider) is an indicator that exposes how many slides it contains, as well as which slide is currently active. This is typically done using bullets.

This again is something we will be able to achieve using a CSS scroll timeline, as shown in this demo created by Fabrizio Calderan:

The active state bullet is injected via .slider nav::before and has an animation set that moves it over the other bullets

/* Styling of the dots */ .slider nav::before, .slider a {   inline-size: 1rem;   aspect-ratio: 1;   border-radius: 50%;   background: #9bc; }  /* Positioning of the active dot */ .slider nav::before {   content: "";   position: absolute;   z-index: 1;   display: block;   cursor: not-allowed;   transform: translateX(0);   animation: dot 1s steps(1, end) 0s forwards; }  /* Position over time of the active dot */ @keyframes dot {   0%      { transform: translateX(0); }   33%      { transform: translateX(calc((100% + var(--gap)) * 1)); }   66%      { transform: translateX(calc((100% + var(--gap)) * 2)); }    100%      { transform: translateX(calc((100% + var(--gap)) * 3)); } }

By attaching a @scroll-timeline onto the slider, the dot that indicates the active state can move as you scroll:

@scroll-timeline slide {   source: selector(#s);   orientation: inline;  }  .slider nav::before {   /* etc. */   animation-timeline: slide; }

The dot only moves after the slide has snapped to its position thanks to the inclusion of a steps() function in the animation. When removing it, it becomes more clear how the dot moves as you scroll

💡 This feels like the final missing piece to Christian Shaefer’s CSS-only carousel.


Back in early 2020, I created a sticky table of contents with scrolling active states. The final part to creating the demo was to use IntersectionObserver to set the active states in the table of contents (ToC) as you scroll up/down the document.

Unlike the carousel indicators demo from above we can’t simply get there by moving a single dot around, as it’s the texts in the ToC that get adjusted. To approach this situation, we need to attach two animations onto each element in the ToC:

  1. The first animation is to visually activate the ToC item when the proper section comes into view at the bottom edge of the document.
  2. The second animation is to visually deactivate the ToC item when the proper section slides out of view at the top edge of the document.
.section-nav li > a {   animation:     1s activate-on-enter linear forwards,     1s deactivate-on-leave linear forwards; }

As we have two animations, we also need to create two scroll timelines, and this for each section of the content. Take the #introduction section for example:

@scroll-timeline section-introduction-enter {   source: auto;   scroll-offsets:     selector(#introduction) end 0,     selector(#introduction) end 1; }  @scroll-timeline section-introduction-leave {   source: auto;   scroll-offsets:     selector(#introduction) start 1,     selector(#introduction) start 0; }

Once both of these timelines are linked to both animations, everything will work as expected:

.section-nav li > a[href"#introduction"] {   animation-timeline:     section-introduction-enter,     section-introduction-leave; }

In closing

I hope I have convinced you of the potential offered by the CSS Scroll-linked Animations specification. Unfortunately, it’s only supported in Chromium-based browsers right now, hidden behind a flag.

Given this potential, I personally hope that—once the specification settles onto a certain syntax—other browser vendors will follow suit. If you too would like to see Scroll-Linked Animations land in other browsers, you can actively star/follow the relevant browser issues.

By actively starring issues, us developers can signal our interest into these features to browser vendors.

The post Practical Use Cases for Scroll-Linked Animations in CSS with Scroll Timelines appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Maps Scroll Wheel Fix

This blog post by Steve Fenton came across my feeds the other day. I’d never heard of HERE maps before, but apparently they are embeddable somehow, like Google Maps. The problem is that you zoom and and out of HERE maps with the scrollwheel. So imagine you’re scrolling down a page, your cursor (or finger) ends up on the HERE map, and now you can’t continue scrolling down the page because that scrolling event is captured by the map and turns into map zooming.

Steve’s solution: put a “coverer” <div> over the map when a scroll event starts on the window, and remove it after a short delay (when scrolling “stops”). That solution resonates with me, as not only have I coded solutions like that in the past for embedded maps, we have a solution like that in place on CodePen today. On CodePen, you can resize the “preview” window, which is an <iframe> of the code you write. If you drag too swiftly, your mouse cursor (or touch event) might trigger movement off of the draggable element, possible onto the <iframe> itself. If that happens, the <iframe> will swallow the event, and the resizing you are trying to do stops working correctly. To prevent this, we put a “covered” <div> over top the <iframe> while you are dragging, and remove it when you stop dragging.

Thinking of maps though, it reminds me Brad Frost’s Adaptive Maps idea documented back in 2012. The idea is that embedding a map on a small screen mobile device just isn’t a good idea. Space is cramped, they can slow down page load, and, like Steve experienced nearly a decade later, they can mess with users scrolling through the page. Brads solution is to serve an image of a map (which can still be API-driven) conditionally for small screens with a “View Map” link that takes them to a full-screen map experience, probably within the map native app itself. Large screens can still have the interactive map, although, I might argue that having the image-that-links-to-map-service might be a smart pattern for any browser with less technical debt.

The post Maps Scroll Wheel Fix appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, ,

How to Create a Shrinking Header on Scroll Without JavaScript

Imagine a header of a website that is nice and thick, with plenty of padding on top and bottom of the content. As you scroll down, it shrinks up on itself, reducing some of that padding, making more screen real estate for other content.

Normally you would have to use some JavaScript to add a shrinking effect like that, but there’s a way to do this using only CSS since the introduction of position: sticky.

Let me just get this out there: I’m generally not a fan of sticky headers. I think they take up too much of the screen’s real estate. Whether or not you should use sticky headers on your own site, however, is a different question. It really depends on your content and whether an ever-present navigation adds value to it. If you do use it, take extra care to avoid inadvertently covering or obscuring content or functionality with the sticky areas — that amounts to data loss.

Either way, here’s how to do it without JavaScript, starting with the markup. Nothing complicated here — a <header> with one descendant <div> which, intern, contains the logo and navigation.

<header class="header-outer">   <div class="header-inner">     <div class="header-logo">...</div>     <nav class="header-navigation">...</nav>   </div> </header>

As far as styling, we’ll declare a height for the parent <header> (120px) and set it up as a flexible container that aligns its descendant in the center. Then, we’ll make it sticky.

.header-outer {   display: flex;   align-items: center;   position: sticky;   height: 120px; }

The inner container contains all the header elements, such as the logo and the navigation. The inner container is in a way the actual header, while the only function of the parent <header> element is to make the header taller so there’s something to shrink from.

We’ll give that inner container, .header-inner, a height of 70px and make it sticky as well.

.header-inner {   height: 70px;   position: sticky;   top: 0;  }

That top: 0? It’s there to make sure that the container mounts itself at the very top when it becomes sticky.

Now for the trick! For the inner container to actually stick to the “ceiling” of the page we need to give the parent <header> a negative top value equal to the height difference between the two containers, making it stick “above” the viewport. That’s 70px minus 120px, leaving with with — drumroll, please — -50px. Let’s add that.

.header-outer {   display: flex;   align-items: center;   position: sticky;   top: -50px; /* Equal to the height difference between header-outer and header-inner */   height: 120px; }

Let’s bring it all together now. The <header> slides out of frame, while the inner container places itself neatly at the top of the viewport.

We can extend this to other elements! How about a persistent alert?

While it’s pretty awesome we can do this in CSS, it does have limitations. For example, the inner and outer containers use fixed heights. This makes them vulnerable to change, like if the navigation elements wrap because the number of menu items exceeds the amount of space.

Another limitation? The logo can’t shrink. This is perhaps the biggest drawback, since logos are often the biggest culprit of eating up space. Perhaps one day we’ll be able to apply styles based on the stickiness of an element…

The post How to Create a Shrinking Header on Scroll Without JavaScript appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Going From Solid to Knockout Text on Scroll

Here’s a fun CSS trick to show your friends: a large title that switches from a solid color to knockout text as the background image behind it scrolls into place. And we can do it using plain ol’ HTML and CSS!

This effect is created by rendering two containers with fixed <h1> elements. The first container has a white background with knockout text. The second container has a background image with white text. Then, using some fancy clipping tricks, we hide the first container’s text when the user scrolls beyond its boundaries and vice-versa. This creates the illusion that the text background is changing.

Before we begin, please note that this won’t work on older versions of Internet Explorer. Also, fixed background images can be cumbersome on mobile WebKit browsers. Be sure to think about fallback behavior for these circumstances.

Setting up the HTML

Let’s start by creating our general HTML structure. Inside an outer wrapper, we create two identical containers, each with an <h1> element that is wrapped in a .title_wrapper.

<header>    <!-- First container -->   <div class="container container_solid">     <div class="title_wrapper">       <h1>The Great Outdoors</h1>     </div>   </div>    <!-- Second container -->   <div class="container container_image">     <div class="title_wrapper">       <h1>The Great Outdoors</h1>     </div>   </div>  </header>

Notice that each container has both a global .container class and its own identifier class — .container_solid and .container_image, respectively. That way, we can create common base styles and also target each container separately with CSS.

Initial styles

Now, let’s add some CSS to our containers. We want each container to be the full height of the screen. The first container needs a solid white background, which we can do on its .container_solid class. We also want to add a fixed background image to the second container, which we can do on its .container_image class.

.container {   height: 100vh; }  /* First container */ .container_solid {   background: white; }  /* Second container */ .container_image {   /* Grab a free image from unsplash */   background-image: url(/path/to/img.jpg);   background-size: 100vw auto;   background-position: center;   background-attachment: fixed; }

Next, we can style the <h1> elements a bit. The text inside .container_image can simply be white. However, to get knockout text for the <h1> element inside container_image, we need to apply a background image, then reach for the text-fill-color and background-clip CSS properties to apply the background to the text itself rather than the boundaries of the <h1> element. Notice that the <h1> background has the same sizing as that of our .container_image element. That’s important to make sure things line up.

.container_solid .title_wrapper h1 {   /* The text background */   background: url(https://images.unsplash.com/photo-1575058752200-a9d6c0f41945?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ);   background-size: 100vw auto;   background-position: center;      /* Clip the text, if possible */   /* Including -webkit` prefix for bester browser support */   /* https://caniuse.com/text-stroke */   -webkit-text-fill-color: transparent;   text-fill-color: transparent;   -webkit-background-clip: text;   background-clip: text;      /* Fallback text color */   color: black; }  .container_image .title_wrapper h1 {   color: white; }

Now, we want the text fixed to the center of the layout. We’ll add fixed positioning to our global .title_wrapper class and tack it to the vertical center of the window. Then we use text-align to horizontally center our <h1> elements.

.header-text {   display: block;   position: fixed;    margin: auto;   width: 100%;   /* Center the text wrapper vertically */   top: 50%;   -webkit-transform: translateY(-50%);       -ms-transform: translateY(-50%);           transform: translateY(-50%); }  .header-text h1 {   text-align: center; }

At this point, the <h1> in each container should be positioned directly on top of one another and stay fixed to the center of the window as the user scrolls. Here’s the full, organized, code with some shadow added to better see the text positioning.

Clipping the text and containers

This is where things start to get really interesting. We only want a container’s <h1> to be visible when its current scroll position is within the boundaries of its parent container. Normally this can be solved using overflow: hidden; on the parent container. However, with both of our <h1> elements using fixed positioning, they are now positioned relative to the browser window, rather than the parent element. In this case using overflow: hidden; will have no effect.

For the parent containers to hide fixed overflow content, we can use the CSS clip property with absolute positioning. This tells our browser hide any content outside of an element’s boundaries. Let’s replace the styles for our .container class to make sure they don’t display any overflowing elements, even if those elements use fixed positioning.

.container {   /* Hide fixed overflow contents */   clip: rect(0, auto, auto, 0);    /* Does not work if overflow = visible */   overflow: hidden;    /* Only works with absolute positioning */   position: absolute;    /* Make sure containers are full-width and height */   height: 100vh;   left: 0;   width: 100%; }

Now that our containers use absolute positioning, they are removed from the normal flow of content. And, because of that, we need to manually position them relative to their respective parent element.

.container_solid {   /* ... */    /* Position this container at the top of its parent element */   top: 0; }  .container_image {   /* ... */  /* Position the second container below the first container */   top: 100vh; }

At this point, the effect should be taking shape. You can see that scrolling creates an illusion where the knockout text appears to change backgrounds. Really, it is just our clipping mask revealing a different <h1> element depending on which parent container overlaps the center of the screen.

Let’s make Safari happy

If you are using Safari, you may have noticed that its render engine is not refreshing the view properly when scrolling. Add the following code to the .container class to force it to refresh correctly.

.container {   /* ... */    /* Safari hack */   -webkit-mask-image: -webkit-linear-gradient(top, #ffffff 0%,#ffffff 100%); }

Here’s the complete code up to this point.

Time to clean house

Let’s make sure our HTML is following accessibility best practices. Users not using assistive tech can’t tell that there are two identical <h1> elements in our document, but those using a screen reader sure will because both headings are announced. Let’s add aria-hidden to our second container to let screen readers know it is purely decorative.

<!-- Second container --> <div class="container container_image" aria-hidden="true">   <div class="title_wrapper">     <h1>The Great Outdoors</h1>   </div> </div>

Now, the world is our oyster when it comes to styling. We are free to modify the fonts and font sizes to make the text just how we want. We could even take this further by adding a parallax effect or replacing the background image with a video. But, hey, at that point, just be sure to put a little additional work into the accessibility so those who prefer less motion get the right experience.

That wasn’t so hard, was it?

The post Going From Solid to Knockout Text on Scroll appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , ,