How to Make a “Raise the Curtains” Effect in CSS

“Raise the curtains” is what I call an effect where the background goes from dark to light on scroll, and the content on top also goes from light to dark while in a sticky position.

Here’s an example where I used the effect on a real-life project:

Want to know how it’s done? I will take you behind the curtain and show you how to raise it, with nothing but HTML and CSS.

Let’s start with the HTML

What we’re making is sort of a simplified “raise the curtain” effect like this:

Showing the raise the curtains effect from dark blue to wheat.
The background and text both change color while scrolling over the element.

I’m keeping things simple for the sake of clarity, but we can stub this out with three elements:

<div class="curtain">
  <div class="invert">
    <h2>Section title</h2>
  </div>
</div>

First, we need a container for the curtain, which we’ll give a .curtain class. Then, inside the .curtain, we have the an .invert child element that will serve as our “sticky” box. And, finally, we have the content inside this box — a good old-fashioned <h2> element for this specific example.

Let’s set up some CSS variables

There are three values we know we’ll need upfront. Let’s make CSS variables out of them so it’s easy to write them into our styles and easily change them later if we need to.

  • --minh – The height of the container
  • --color1 – The light color
  • --color2 – The dark color
:root {
  --minh: 98vh;
  --color1: wheat;
  --color2: midnightblue;
}

Time to draw the curtain

Next, we can define our .curtain element using the following techniques:

  • A linear-gradient for the “split” background
  • min-height for the extra space at the bottom of the container

We use the ::after pseudo-element to add the extra space to the bottom. This way, our “sticky” content will actually stick to the container while scrolling past the ::after element. It’s an illusion.

.curtain {
  /** create the "split" background **/
  background-image: linear-gradient(to bottom, var(--color2) 50%, var(--color1) 50%);
}

/** add extra space to the bottom (need this for the "sticky" effect) **/
.curtain::after {
  content: "";
  display: block;
  min-height: var(--minh);
}

Making sticky content

Next up, we need to make our content “sticky” in the sense that it sits perfectly inside the container as the background and text swap color values. In fact, we already gave the .curtain‘s child element an .invert class that we can use as the sticky container.

Stay with me for a moment — here’s how this is going to play out:

  • position: sticky and top define the stickiness and where it sticks.
  • mix-blend-mode: difference blends the color of the content inside the <h2> element into the .curtain‘s background gradient.
  • display: flex centers the content for presentation.
  • min-height defines the height of the container and allows for the extra space at the bottom.
  • color sets the color of the h2 heading.

Now to put that into CSS code!

.invert {
  /** make the content sticky **/
  position: sticky;
  top: 20px;

  /** blend the content with the contrast effect **/
  mix-blend-mode: difference;

  /** center the content **/
  display: flex;
  align-items: center;
  justify-content: center;
  
  /** set the minimum height of the section **/
  min-height: var(--minh);
}

h2 {
  /** set the color of the text **/
  color: var(--color1);
}

There are many things going on here, so let’s explain each one of them.

First, we have a sticky position that is self-explanatory and flexbox to help center the content. Nothing new or particularly tricky about this.

The content’s height is set using CSS variable and the value is the same height value as the .curtain::after pseudo-element.

The mix-blend-mode: difference declaration blends our content with the background. The difference value is complicated, but you might visualize it like inverted text color against the background. Here’s a nice demo from the CSS-Tricks Almanac showing off the different mix-blend-mode values:

To make the blending work, we need to set the color of our heading. In this case, we’re assigning a light color value (wheat) to the --color1 variable.

“Raise the Curtains” Demo

Gotchas

I experienced a few problems while working out the details of the “raise the curtain” effect. If you want to add images to the “sticky” content, for example, avoid using images that don’t look good when their colors are inverted. Here’s a quick demo where I made a simple SVG and transparent PNG image, and it looks good.

Another gotcha: there’s no way to set mix-blend-mode: difference on specific child elements, like headings, while avoiding the effect on images. I discovered there are several reasons why it doesn’t work, the first of which is that position: sticky cancels the blending.

The same goes when using something like transform: skewY on the container to add a little “tilt” to things. I suspect other properties don’t play well with the blending, but I didn’t go that far to find out which ones.

Here’s the demo without scrolling that removes the troubling properties:

Curtain call!

I enjoyed building this component, and I always love it when I can accomplish something using only HTML and CSS, especially when they work smoothly on every browser.

What will make with it? Is there a different way you would approach a “raise the curtain” effect like this? Let me know in the comments!


How to Make a “Raise the Curtains” Effect in CSS originally published on CSS-Tricks. You should get the newsletter.

A Clever Sticky Footer Technique

Upon hearing “sticky footer” these days, I would think most people imagine a position: sticky situation where a footer element appears fixed on the screen while in the scrolling context of some parent element.

That’s not quite what I’m talking about here. “Sticky footers” were a UI concept before position: sticky existed and they mean something slightly different. The idea is that they stick to the bottom of the screen, even when the content of the page isn’t enough to push them there. But if there is enough content, they are happily pushed down.

We covered five ways to do this in years past, which included techniques that are somewhat modern, including calc(), flexbox, and CSS Grid.

Enter a sixth challenger! Reader Sílvio Rosa wrote in with this:

(Probably easiest to check out on a medium-sized desktop screen, which is kinda where sticky footers matter the most anyway.)

It’s pretty much just this:

html, body { height: 100%;}

body > footer {
  position: sticky;
  top: 100vh;
}

What I like about it is that it doesn’t require any special extra wrapper for non-footer content.

It’s also a little brain-bending. When I see top: 100vh; I think well that won’t work because it will push the footer outside the viewable area. But that’s the clever bit. It will do that no matter how big the footer is (no magic numbers), and then the sticky positioning will “suck it back up” to stick along the bottom edge. But it will never overlap content, so it’s happy to be pushed down below content, which is a core tenant of the sticky footer pattern.


The post A Clever Sticky Footer Technique appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Sticky Headers: 5 Ways to Make Them Better

Page Laubheimer says that if you’re going to do a sticky header…

  1. Keep it small.
  2. Visually contrast it with the rest of the page.
  3. If it’s going to move, keep it minimal. (I’d say, respect prefers-reduced-motion.)
  4. Consider “partially persistent headers.” (Jemima Abu calls it a Smart Navbar.)
  5. Actually, maybe don’t even do it.

I generally like the term “sticky” header, because it implies you should use position: sticky for them, which I think you should. It used to be done with position: fixed, but that was trickier to pull off since the header would move in-and-out of flow of the document. Using sticky positioning helps reserve that space automatically without JavaScript or magic numbers.

Direct Link to ArticlePermalink


The post Sticky Headers: 5 Ways to Make Them Better appeared first on CSS-Tricks.

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

Creating a Smart Navbar With Vanilla JavaScript

Sticky, or fixed, navigation is a popular design choice because it gives users persistent access to navigate the site. On the other hand, it takes up space on the page and sometimes covers content is a way that’s less than appealing.

A possible solution? Smart navigation.

Let’s define “smart navigation” as:

  1. Visible at the top of the page
  2. Visible when the user moves up the page (wherever they may have scrolled to)
  3. Hidden when the user moves down the page

Here’s an example of how that might work:

It‘s all the convenience of sticky positioning, with an added fullscreen benefit. This sort of smart navigation is already commonly (think of the URL bar in many mobile browsers), but is sometimes a hassle to implement without a library or plugin. So, in this article, we’ll discuss how to build one using CSS and vanilla JavaScript.

Side note: People have different definitions of what scrolling down a page means (imagine how some trackpad preferences scroll the page up when you move your fingers down). For the purposes of this article, scrolling down refers to moving towards the bottom of the page.

Let’s look at the code

Here’s some example HTML. Our smart navigation will be the <nav> which sits above the <main>:

<nav>
  <div class="logo">
    Logo
  </div>
  <div class="links">
    <a href="#">Link 1</a>
    <a href="#">Link 2</a>
    <a href="#">Link 3</a>
    <a href="#">Link 4</a>
  </div>
</nav>
<main>
  <!--Place the content of your page here-->
</main>

It’s important to note that elements are only sticky relative to their parent container. The parent container of <nav> should be the body tag; it shouldn’t be placed within another tag on the page.

The CSS for our smart navigation looks like this:

nav {
  position: sticky;
  top: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  padding: 1.5rem 2rem;
  background-color: #eaeaea;
}

Now we need to detect when our user is scrolling the page and the direction of their scrolling. A user is scrolling down if the value of their last scroll position is less than the value of their current scroll position. Breaking the logic down, we’ll need to:

  1. Define a variable to store the previous scroll position
  2. Assign a variable to detect the current scroll position set to the scroll offset of the page

If the current scroll position is greater than the previous scroll position, then the user is scrolling downwards. Let’s call our function isScrollingDown:

let previousScrollPosition = 0;

const isScrollingDown = () => {
  let currentScrolledPosition = window.scrollY || window.pageYOffset;
  let scrollingDown;

  if (currentScrolledPosition > previousScrollPosition) {
    scrollingDown = true;
  } else {
    scrollingDown = false;
  }
  previousScrollPosition = currentScrolledPosition;
  return scrollingDown;
};

Here’s a visual representation of how this function works:

With this logic, we’re able to detect when the page is scrolling down so we can use this to toggle our nav styling:

const nav = document.querySelector('nav');

const handleNavScroll = () => {
  if (isScrollingDown()) {
    nav.classList.add('scroll-down');
    nav.classList.remove('scroll-up')
  } else {
    nav.classList.add('scroll-up');
    nav.classList.remove('scroll-down')
  }
}

If the user is scrolling down, we’ll assign a .scroll-down class that contains our styling method for when the page is moving downward. We can update our <nav> CSS to this:

nav {
  /* default styling */
  transition: top 500ms ease-in-out;
}

nav.scroll-up {
  top: 0;
}

nav.scroll-down {
  top: -100%;
}

With this styling, the top property value of <nav> is set to -100% of the page height so it slides out of view. We could also choose to handle our styling with translate or by fading it out — whatever animation works best.

Performance

Whenever we’re working with scroll event listeners, performance is something that should immediately come to mind. Right now, we’re calling our function every time the user scrolls the page, but we don’t need to detect each pixel movement.

For this case, we can implement a throttle function instead. A throttle function is a higher order function that acts as a timer for the function passed into it. If we throttle a scroll event with a timer of 250ms, the event will only be called every 250ms while the user scrolls. It’s a great way to limit the number of times we call the function, helping with the performance of the page.

David Corbacho goes deeper into throttle implementations in this article.

A simple throttle implementation in JavaScript looks like this:

// initialize a throttleWait variable
var throttleWait;

const throttle = (callback, time) => {
  // if the variable is true, don't run the function
  if (throttleWait) return;

  // set the wait variable to true to pause the function
  throttleWait = true;

  // use setTimeout to run the function within the specified time
  setTimeout(() => {
    callback();

    // set throttleWait to false once the timer is up to restart the throttle function
    throttleWait = false;
  }, time);
}

Then we can include our handleNavScroll function inside a throttle:

window.addEventListener("scroll", () => {
  throttle(handleNavScroll, 250)
});

With this implementation, the handleNavScroll function is only called once every 250ms.

Accessibility

Whenever implementing a custom feature in JavaScript, we must always take accessibility into concern. One such issue is ensuring that <nav> is visible when it’s in focus. Browsers tend to scroll to the part of the page that currently has focus by default, but there can be certain complications when working with scroll events.

A way to ensure that <nav> is always visible is to update the CSS to account for focus. Now our CSS looks like this:

nav.scroll-up,
nav:focus-within {
  top: 0;
}

Unfortunately, the focus-within selector isn’t fully supported across all browsers. We can include a JavaScript fallback for it:

const handleNavScroll = () => {
  if (isScrollingDown() && !nav.contains(document.activeElement))) {
    nav.classList.add('scroll-down');
    nav.classList.remove('scroll-up')
  } else {
    nav.classList.add('scroll-up');
    nav.classList.remove('scroll-down')
  }
}

In this updated function, we only apply the scroll-down class if the user is scrolling down the page and the <nav> doesn’t currently have any element with focus in it.

Another aspect of accessibility is the consideration that some users may not want to have any animation on the page. That’s something we can detect and respect with the prefers-reduced-motion CSS media query. We can update this method in JavaScript and prevent our function from running at all if a user prefers reduced motion:

const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");

window.addEventListener("scroll", () => {
  if (mediaQuery && !mediaQuery.matches) {
    throttle(handleNavScroll, 250)
  }
});

Wrapping up

So, there we have it: a smart navigation implementation with plain CSS and vanilla JavaScript. Now users have persistent access to navigate the site without losing real estate in a way that blocks content.

Plus, the benefit of a custom implementation like this is that we get a delightful user experience that isn’t over-engineered or sacrifices open performance or accessibility.


The post Creating a Smart Navbar With Vanilla JavaScript 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.

A table with both a sticky header and a sticky first column

We’ve covered that individual <table> cells, <th> and <td> can be position: sticky. It’s pretty easy to make the header of a table stick to the top of the screen while scrolling through a bunch or rows (like this demo).

But stickiness isn’t just for the top of the screen, you can stick things in any scroll direction (horizontal is just as fun). In fact, we can have multiple sticky elements stuck in different directions inside the same element, and even single elements that are stuck in multiple directions.

Here’s a video example of a table that sticks both the header and first column:

Why would you do that? Specifically for tabular data where cross-referencing is the point. In this table (which represents, of course, the scoring baseball game where somehow 20 teams are all playing each other at once because that’s how baseball works), it “makes sense” that you wouldn’t want the team name or the inning number to scroll away, as you’d lose context of what you’re looking at.

Not all tables need to be bi-directionally cross-referenceable. A lot of tables can smash rows into blocks on small screens for a better small-screen experience.

The “trick” at play here is partially the position: sticky; usage, but moreso to me, how you have to handle overlapping elements. A table cell that is sticky needs to have a background, because otherwise we’ll see overlapping content. It also needs proper z-index handling so that when it sticks in place, it’ll be on top of what it is supposed to be on top of. This feels like the trickiest part:

  • Make sure the tbody>th cells are above regular table cells, so they stay on top during a horizontal scroll.
  • Make sure the thead>th cells are above those, for vertical scrolling.
  • Make sure the thead>th:first-child cell is the very highest, as it needs to be above the body cells and it’s sibling headers again for horizontal scrolling.

A bit of a dance, but it’s doable.

High five to Cameron Clark who emailed me demoed this and showed me how cool it is. And indeed, Cameron, it is cool. When I shared that around, Estelle Weyl showed me a demo she made several years ago. That feels about right, Estelle is always a couple of years ahead of me.


The post A table with both a sticky header and a sticky first column appeared first on CSS-Tricks.

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

How to Get Sticky and Full-Bleed Elements to Play Well Together

I had a unique requirement the other day: to build a layout with full-bleed elements while one element stays stuck to the top. This ended up being rather tricky to pull off so I’m documenting it here in case anyone needs to re-create this same effect. Part of the trickiness was dealing with logical positioning on small screens as well.

It’s tough to describe the effect, so I recorded my screen to show what I mean. Pay special attention to the main call to action section, the one with the “Try Domino Today” header.

The idea is to display the main call to action on the right side while users scroll past other sections on larger viewports. On smaller viewports, the call to action element has to display after the main hero section with the “Start your trial” header.

There are a two main challenges here:

  • Make full-bleed elements that don’t interfere with the sticky element
  • Avoid duplicating the HTML

Before we dive into a couple of possible solutions (and their limitations), let’s first set up the semantic HTML structure.

The HTML

When building these kinds of layouts, one might be tempted to build duplicate call-to-action sections: one for the desktop version and the other for the mobile version and then toggle the visibility of them when appropriate. This avoids having to find the perfect place in the HTML and needing to apply CSS that handles both layout needs. I must admit, I am guilty of doing that from time to time. But this time, I wanted to avoid duplicating my HTML.

The other thing to consider is that we’re using the sticky positioning on the .box--sticky element, which means it needs to be the sibling of other elements, including full-bleed ones, for it to properly work.

Here’s the markup:

<div class="grid">

  <div class="box box--hero">Hero Box</div>

  <div class="box box--sticky">Sticky Box</div>

  <div class="box box--bleed">Full-bleed Box</div>
  <div class="box box--bleed">Full-bleed Box</div>
  <!-- a bunch more of these -->

</div>

Let’s get sticky

Making sticky elements in a CSS grid layout is pretty straightforward. We add position: sticky to the .box--sticky element with a top: 0 offset, indicating where it starts to stick. Oh, and notice that we’re only making the element sticky on viewports larger that 768px.

@media screen and (min-width: 768px) {
  .box--sticky {
    position: sticky;
    top: 0;
  }
}

Beware that there is a known issue with sticky positioning in Safari when it’s used with overflow: auto. It is documented over at caniuse in the known issues section:

A parent with overflow set to auto will prevent position: sticky from working in Safari.

Nice, that was easy. Let’s solve the challenge of full-bleed elements next.

Solution 1: Pseudo-elements

The first solution is something I use often: absolutely positioned pseudo-elements that stretch from one side to side. The trick here is to use a negative offset.

If we are talking about centered content, then the calculation is quite straightforward:

.box--bleed {
  max-width: 600px;
  margin-right: auto;
  margin-left: auto;
  padding: 20px;
  position: relative; 
}

.box--bleed::before {
  content: "";
  background-color: dodgerblue; 
  position: absolute;
  top: 0;
  bottom: 0;
  right: calc((100vw - 100%) / -2);
  left: calc((100vw - 100%) / -2);
}

In short, the negative offset is the width of the viewport, 100vw, minus the width of the element, 100%, and then divided by -2, because we need two negative offsets.

Beware that there is a known bug when using 100vw, that is also documented over at caniuse:

Currently all browsers but Firefox incorrectly consider 100vw to be the entire page width, including vertical scroll bar, which can cause a horizontal scroll bar when overflow: auto is set.

Now let’s make full-bleed elements when the content is not centered. If you watch the video again, notice that there is no content below the sticky element. We don’t want our sticky element to overlap the content and that is the reason why be don’t have centered content in this particular layout.

First, we are going to create the grid:

.grid {
  display: grid;
  grid-gap: var(--gap);
  grid-template-columns: var(--cols);
  max-width: var(--max-width);
  margin-left: auto;
  margin-right: auto;
}

We’re using custom properties which allows us to redefine the maximum width, the gap, and grid columns without redeclaring the properties. In other words, instead of redeclaring the grid-gap, grid-template-columns, and max-width properties, we are re-declaring variable values:

:root {
  --gap: 20px;
  --cols: 1fr;
  --max-width: calc(100% - 2 * var(--gap));
}

@media screen and (min-width: 768px) {
  :root {
    --max-width: 600px;
    --aside-width: 200px;
    --cols: 1fr var(--aside-width);
  }
}

@media screen and (min-width: 980px) {
  :root {
    --max-width: 900px;
    --aside-width: 300px;
  }
}

On viewports that are 768px wide and above, we have defined two columns: one with a fixed width, --aside-width, and one with that fills the remaining space, 1fr, as well as maximum width of the grid container, --max-width.

On viewports smaller than 768px, we have defined a single column and the gap. The maximum width of the grid container is 100% of the viewport, minus gaps on each side.

Now comes the fun part. The content isn’t centered on bigger viewports, so the calculation isn’t as straightforward as you might think. Here’s how it looks:

.box--bleed {
  position: relative;
  z-index: 0;
}

.box--bleed::before {
  content: "";
  display: block;
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc((100vw - (100% + var(--gap) + var(--aside-width))) / -2);
  right: calc(((100vw - (100% - var(--gap) + var(--aside-width))) / -2) - (var(--aside-width)));
  z-index: -1;
}

Instead of using 100% of the parent’s width, we’re taking into account the widths of the gap and the sticky element. That means width of the content in full-bleed elements will not exceed the bounds of the hero element. That way, we ensure the sticky element won’t overlap any important piece of information.

The left offset is simpler because we only need to subtract the width of the element (100%), the gap (--gap), and the sticky element (--aside-width) from the viewport width (100vw).

left: (100vw - (100% + var(--gap) + var(--aside-width))) / -2);

The right offset is more complicated because we have to add the width of the sticky element to the previous calculation, --aside-width, as well as the gap, --gap:

right: ((100vw - (100% + var(--gap) + var(--aside-width))) / -2) - (var(--aside-width) + var(--gap));

Now we are sure the sticky element doesn’t overlap any content in full-bleed elements.

Here’s the solution with a horizontal bug:

And here’s the solution with a horizontal bugfix:

The fix is to hide overflow on the x-axis of the body, which might be a good idea in general anyway:

body {
  max-width: 100%;
  overflow-x: hidden;
}

This is a perfectly viable solution and we could end here. But where’s the fun in that? There’s usually more than one way to accomplish something, so let’s look at another approach.

Solution 2: Padding calculations

Instead of using a centered grid container and pseudo elements, we could achieve the same effect by configuring our grid. Let’s start by defining the grid just as we did last time:

.grid {
  display: grid;
  grid-gap: var(--gap);
  grid-template-columns: var(--cols);
}

Again, we are using custom properties to define the gap and the template columns:

:root {
  --gap: 20px;
  --gutter: 1px;
  --cols: var(--gutter) 1fr var(--gutter);
}

We’re showing three columns on viewports smaller than 768px. The center column takes as much space as possible, while the other two are used only to force the horizontal gap.

@media screen and (max-width: 767px) {
  .box {
    grid-column: 2 / -2;
  }
}

Note that all grid elements are placed in the center column.

On viewports bigger than 768px, we are defining a --max-width variable that limits the width of the inner columns. We’re also defining --aside-width, the width of our sticky element. Again, this way we ensure the sticky element won’t be positioned over any content inside the full-bleed elements.

:root {
  --gap: 20px;
}

@media screen and (min-width: 768px) {
  :root {
    --max-width: 600px;
    --aside-width: 200px;
    --gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));
    --cols: var(--gutter) 1fr var(--aside-width) var(--gutter);
  }
}

@media screen and (min-width: 980px) {
  :root {
    --max-width: 900px;
    --aside-width: 300px;
  }
}

Next, we are calculating the gutter width. The calculation is:

--gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));

…where 100% is the viewport width. First, we are subtracting the maximum width of the inner columns from the width of the viewport. Then, we are dividing that result by 2 to create the gutters. Finally, we are subtracting the grid’s gap to get the correct width of the gutter columns.

Now let’s push the .box--hero element over so it starts at the first inner column of the grid:

@media screen and (min-width: 768px) {
  .box--hero {
    grid-column-start: 2;
  }
}

This automatically pushes the sticky box so it starts right after the hero element. We could also explicitly define the placement of the sticky box, like this:

.box--sticky {
  grid-column: 3 / span 1;
}

Finally, let’s make the full-bleed elements by setting grid-column to 1 / -1. That tells the elements to start the content at the first grid item and span through to the last one.

@media screen and (min-width: 768px) {  
  .box--bleed {
    grid-column: 1 / -1;
  }
}

To center the content, we are going to calculate left and right padding. The left padding is equal to the size of the gutter column, plus the grid gap. The right padding is equal to the size of the left padding, plus another grid gap as well as the width of the sticky element.

@media screen and (min-width: 768px) {
  .box--bleed {  
    padding-left: calc(var(--gutter) + var(--gap));
    padding-right: calc(var(--gutter) + var(--gap) + var(--gap) + var(--aside-width));
  }
}

Here’s the final solution:

I prefer this solution to the first one because it isn’t using buggy viewport units.


I love CSS calculations. Using mathematical operations is not always straightforward, especially when combining different units, like 100%. Figuring out what 100% means is half of the effort.

I also love solving simple, yet complicated layouts, like this one, using only CSS. Modern CSS has native solutions — like grid, sticky positioning and calculations — that remove complicated and somewhat heavy JavaScript solutions. Let’s leave the dirty work for the browser!

Do you have a better solution or different approach for this? I would be happy to hear about it.


The post How to Get Sticky and Full-Bleed Elements to Play Well Together appeared first on CSS-Tricks.

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

A Dynamically-Sized Sticky Sidebar with HTML and CSS

Creating page content that sticks to the viewport as you scroll, something like a jump-to-anchor menu or section headings, has never been easier. Throw a position: sticky into your CSS ruleset, set the directional offset (e.g. top: 0) and you’re ready to impress your teammates with minimal effort. Check out this CSS-Tricks article to see some real fancy sticky positioning use cases.

But sticky positioning can get a bit tricky, particularly when it comes to height and the dangerous situation of hiding content in a position that can’t be scrolled to. Let me set the stage and show you the problem and how I fixed it.

I recently worked on a desktop layout that we’re all familiar with: a main content area with a sidebar next to it. This particular sidebar contains action items and filters that are pertinent to the main content. As the page section is scrolled, this component remains fixed to the viewport and contextually accessible.

The layout styling was as easy to implement as I had mentioned earlier. But there was a catch: The height of the component would vary based on its content. I could have capped it with a max-height and set overflow-y: auto to make the component content scrollable. This worked well on my laptop screen and my typical viewport height, but in a smaller viewport with less vertical real estate, the sidebar’s height would exceed the viewport.

When the sticky sidebar height is larger than the viewport, some of its content becomes inaccessible until reaching the bottom of the container, when the element is no longer sticky.

That’s where things got tricky.

Thinking through solutions

I initially considered reaching for a media query. Perhaps I could use a media query to remove the sticky positioning and have the component sit relative to the top of the sidebar container. This would grant access to the entirety of its content. Otherwise, when scrolling the page, the sticky component’s content is cut off at the bottom of the viewport until I reach the end of its parent section.

Then I remembered that the height of the sticky component is dynamic.

What magic value could I use for my media query that would handle such a thing? Perhaps instead I could write a JavaScript function to check if the component flows beyond the viewport boundaries on page load? Then I could update the component’s height…

That was a possibility.

But what if the user resizes their window? Should I use that same function in a resize event handler? That doesn’t feel right. There must be a better way to build this.

Turns out there was and it involved some CSS trickery to get the job done!

Setting up the page section

I started with a flex display on the main element. A flex-basis value was set to the sidebar for a fixed desktop width. Then the article element filled the rest of the available horizontal viewport space.

If you’re curious about how I got the two containers to stack for smaller viewports without a media query, check out The Flexbox Holy Albatross trick.

I added align-self: start to the sidebar so its height wouldn’t stretch with the main article (stretch  is the default value). This gave my positioning properties the ability to cast their magic:

.sidebar {
  --offset: var(--space);
  /* ... */
  position: sticky;
  top: var(--offset);
}

Check that out! With these two CSS properties, the sidebar element sticks to the top of the viewport with an offset to give it some breathing room. Notice that the top value is set to a scoped CSS custom property. The --offset variable can now be reused on any child element inside the sidebar. This will come in handy later when setting the sticky sidebar component’s maximum height.

You can find a list of global CSS variable declarations in the CodePen demo, including the --space variable used for the offset value in the :root ruleset.

The sticky sidebar

Keep in mind that the component itself is not what is sticky; it’s the sidebar itself. General layout and positioning should typically be handled by the parent. This gives the component more flexibility and makes it more modular to use in other areas of the application.

Let’s dive into the anatomy of this component. In the demo, I’ve removed the decorative properties below to focus on the layout styles:

.component {
  display: grid;
  grid-template-rows: auto 1fr auto;
}


.component .content {
  max-height: 500px;
  overflow-y: auto;
}
  • This component uses CSS Grid and the pancake stack idea from 1-Line Layouts to configure the rows of this template. Both the header and footer (auto) adjust to the height of their children while the content (1fr, or one fraction unit) fills up the rest of the open vertical space.
  • A  max-height on the content limits the component’s growth on larger screen sizes. This is unnecessary if it’s preferred that the component stretch to fill the viewport height.
  • overflow-y: auto allows the content to be scrolled when necessary.

When the component is being used in the sidebar, a max-height is needed so that it doesn’t exceed the viewport height. The --offset previously scoped to the .sidebar class is doubled to create a margin on the bottom of the element that matches the top offset of the sticky sidebar:

.sidebar .component {
  max-height: calc(100vh - var(--offset) * 2);
}

That wraps up the assembly of this sticky sidebar component! After some decorative styles were applied, this prototype became ready for testing and review. Give it a try! Open up the demo in CodePen and click on the grid items to add them to the sidebar. Resize your browser window to see how the component flexes with the viewport while staying in view as you scroll the main content section.


This layout may work well on a desktop browser, but isn’t entirely ideal for smaller devices or viewport widths. However, the code here provides a solid foundation that makes it easy to add improvements to the UI.

One simple idea: A button could be affixed to the viewport window that, when clicked, jumps the page down to the sidebar content. Another idea: The sidebar could be hidden off-screen and a toggle button could slide it in from the left or right. Iteration and user testing will help drive this experience in the right direction.


The post A Dynamically-Sized Sticky Sidebar with HTML and CSS appeared first on CSS-Tricks.

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

Stacked Cards with Sticky Positioning and a Dash of Sass

The other day, I spotted this particularly lovely bit from Corey Ginnivan’s website where a collection of cards stack on top of one another as you scroll.

I started wondering how much JavaScript this would involve and how you’d go about making it when I realized — ah! — this must be the work of position: sticky and a tiny amount of Sass. So, without diving into how Corey did this, I decided to take a crack at it myself.

First up, some default styles for the cards:

body {
  background: linear-gradient(#e8e8e8, #e0e0e0);
}

.wrapper {
  margin: 0 auto;
  max-width: 700px;
}

.card {
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 10px;
  box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1);
  color: #333;
  padding: 40px;
}

Next, we need to make each card sticky to the top of the wrapper. We can do that like this:

.card {
  position: sticky;
  top: 10px;
  // other card styles
}

And that leaves us with this:

But how do we get each of these elements to look like a stack on top of one another? Well, we can use some fancy Sass magic to fix the position of each card. First we’ll loop over every card element and then change the value with each iteration:

@for $i from 1 through 8 {
  .card:nth-child(#{$i}n) {
    top: $i * 20px;
  }
}

Which results in this demo, which is totally charming, if I do say so myself:

And there we have it! We could make a few visual changes here to improve things. For example, the box-shadow and color of each card, just like Corey’s example. But I wanted to keep experimenting here. What if we switch the order of the cards and made them horizontal instead?

We already do that on this very website:

After experimenting for a little bit I changed the order of the cards with flexbox and made each item slide in from right to left:

.wrapper {
  display: flex;
  overflow-x: scroll;
}

.card {
  height: 60vh;
  min-width: 50vw;
  position: sticky;
  top: 5vh;
  left: 10vw;
}

But I also wanted to make each of the cards come in at different angles so I updated the Sass loop with the random function:

@for $i from 1 through 8 {
  .card:nth-child(#{$i}n) {
    left: $i * 20px;
    left: random(200) + $i * 1px;
    top: random(130) + $i * 1px;
    transform: rotate(random(3) - 2 * 1deg);
  }
}

That’s the bulk of the changes and that results in the following:

Pretty neat, eh? I love position: sticky; so much.


The post Stacked Cards with Sticky Positioning and a Dash of Sass appeared first on CSS-Tricks.

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

A “new direction” in the struggle against rightward scrolling

You know those times you get a horizontal scrollbar when accidentally placing an element off the right edge of the browser window? It might be a menu that slides in or the like. Sometimes we to overflow-x: hidden; on the body to fix that, but that can sometimes wreck stuff like position: sticky;.

Well, you know how if you place an element off the left edge of a browser window, it doesn’t do that? That’s “data loss” and just how things work around here. It actually has to do with the direction of the page. If you were in a RTL situation, it would be the left edge of the browser window causing the overflow situation and the right edge where it doesn’t.

Emerson Loustau leverages that idea to solve a problem here. I’d be way too nervous messing with direction like this because I just don’t know what the side effects would be. But, hey, at least it doesn’t break position: sticky;.

Direct Link to ArticlePermalink

The post A “new direction” in the struggle against rightward scrolling appeared first on CSS-Tricks.

Indicating Scroll Position on a Page With CSS

Scrolling is something we all know and do on the web to the extent that it’s an expectation or perhaps even a habit, like brushing our teeth. That’s probably why we don’t put too much thought into designing the scrolling experience — it’s a well-known basic function. In fact, the popular “there is no fold” saying comes from the idea that people know how to scroll and there is no arbitrary line that people don’t go under.

Scroll-based features tend to involve some bespoke concoction of CSS and JavaScript. That’s because there simply aren’t that many native features available to do it. But what if we could accomplish something that only uses CSS? 

Take this ingenious horizontal scrollbar with CSS, for instance. I want to do something similar, but to indicate scrolled sections rather than capture continuous scrolling. In other words, rather than increasing the length of the indicator during scroll, I only want to increase the length when a certain section of the page has been reached.

Like this:

Here’s my plan: Each section carries an indicator that’s undetectable until it reaches the top of the screen. That’s where it becomes visible by changing color and sticks to the top of the viewport.

The exact opposite should happen in reverse: the indicator will follow along when scrolling back up the screen, camouflaging itself back to being undetected to the naked eye.

There are two key parts to this. The first is for the indicator to change color when it’s near the top of the screen. The second is for the indicator to stay put at the top of the screen and come down only when its section is scrolled down to.

The second one is easy to do: we use position: sticky; on our elements. When a page is scrolled, a sticky element sticks to a given position on the screen within its parent container.

That brings us to changing colors. Since the background of an HTML document is white by default, I’m keeping white as the base color for the demo. This means the indicator should look white when it’s over the base color and turn to some other color when it’s over the indicator bar at the top of the screen.

The dashed indicator is currently invisible, but becomes visible when it sticks to the top and blends with the background color of the indicator container.

This is where CSS blend modes come into play. They give us so many options to create a variety of color amalgams. I’m going to go with the overlay value. This one is quite dynamic in nature. I won’t explain the blend in depth (because the CSS-Tricks Alamanac already does a good job of that) but taking this demo into account, I’ll say this: when the background color is white the resulting foreground color is white; and when the background is some other color, the resulting color is darker or lighter, depending on the color it’s mixed with.

The indicator stops in the demo are black. But, because of the blend, we see them as white because they are on a white background. And when they are over the indicator container element, which is a lovely shade of violet, we see a dark violet indicator stop, because we’re mixing the indicator stop’s black with the indicator container’s violet.

Starting with the HTML:

<div id="passageWrapper">
  <strong>Sections Scrolled ↴</strong>
  <!-- Indicator container -->
  <div id="passage"></div>
</div>


<!-- Indicator stop -->
<div class=passageStops></div>


<!-- First Section -->
<div class="sections">
  <!-- Content -->
</div>


<!-- Another indicator stop -->
<div class="passageStops"></div>


<!-- Second Section -->
<div class="sections">
  <!-- Content -->
</div>


<!-- Another indicator stop -->
<div class="passageStops"></div>


<!-- Third Section -->
<div class="sections">
  <!-- Content -->
</div>

Pretty straightforward, right? There’s a sticky container at the very top that holds the indicators when they reach the top.  From there, we have three sections of content, each one topped with an indicator that will stick to the top with the indicator and blend with it.

Here’s the CSS:

.passageStops {
  background-color: black; /* Each indicator stop is black */
  mix-blend-mode: overlay; /* This makes it appear white on a white background */
  width: 33.3%; /* Three sections total, so each section is one-third */
  top: calc(1em + 3px);
}


#passage, 
.passageStops{
  height: 10px;
}


#passageWrapper,
.passageStops {
  position: sticky; /* The container and stops should stick to the top */
  z-index: 1; /* Make sure the indicator and stops stay at the forefront */
}


#passage {
  background: violet; /* Will blend with black to make a darker violet indicator */
  margin: 0 0 20px 0;
}


#passageWrapper{
  background-color: white; /* Make sure we're working with white to hide indicator stops */
  height: 40px;
  top: 0px;
}


/* Each stop will shift one-third the width of the indicator container to cover the whole thing when the last section is reached. */
.passageStops:nth-child(4){ margin-left: 33.3%; }
.passageStops:nth-child(6){ margin-left: 66.6%; }


/* More styling, blah blah. */

The indicators (.passageStops) are black. But the overlay blend mode makes them appear white when it blends with the white background under it. Since there are three sections, each indicator is of one-third width.

The indicators have position: sticky; with a top distance value. This means the indicators will stick once they reach the calculated position from the top of the screen. When that happens, the black indicators that appeared white blend with the violet indicator container, which makes them appear to be a dark violet, representing the new scroll position on the page.

The reverse is also true. When an indicator loses its sticky position, it will move from the violet background of the indicator bar to the white background of the page, hiding it once again… like it was never there!

Here’s the demo again:

That’s it. You can perhaps further experiment with this by having a non-white background with another blend mode, or a gradient for the indicator bar or stops.

The post Indicating Scroll Position on a Page With CSS appeared first on CSS-Tricks.

Getting Fancy with position: sticky;

Mike Solomon worked on a fancy scrollytelling post for Esquire and blogged about it. It has GIFs of each step along the way of figuring out not just position: sticky; but also using negative margins, wrapper divs, backgrounds, and even a smidge of JavaScript measuring to get it all right.

What it doesn't have is any isolated demo of the effect. I figured I'd give a crack at reverse engineering it.

Here's mine, which I'll call "Sticky Figcaption with Protruding Figure":

That demo is full of magic numbers to make the exit do the "tuck behind" effect. If that's not important, this version is much cleaner.

Probably not quiteas Mike had it, but I'm not privy to the exact details he was going for in the blog post. His final GIF is:

Here's a quick video I'll shoot from the article itself in case that inspires you to figure out a different approach:


Erp! I actually spoke with Mike about all this, and he says that the main takeaway from all this (which flew right over my head — sorry Mike!) is that "sticky isn't just for the top of the screen." Notice in the final product how the sticky element becomes sticky long before it becomes the element at the top of the screen. It's more like the middle of the screen. That's what the top value is for with position: sticky; but, in this demo where the goal is to have it slide in and out of an image, it gets tricky.

After some back and forth forking...

Direct Link to ArticlePermalink

The post Getting Fancy with position: sticky; appeared first on CSS-Tricks.

Position Sticky and Table Headers

You can't position: sticky; a <thead>. Nor a <tr>. But you can sticky a <th>, which means you can make sticky headers inside a regular ol' <table>. This is tricky stuff, because if you didn't know this weird quirk, it would be hard to blame you. It makes way more sense to sticky a parent element like the table header rather than each individiaul element in a row.

The issue boils down to the fact that stickiness requires position: relative to work and that doesn't apply to <thead> and <tr> in the CSS 2.1 spec.

There are two very extreme reactions to this, should you need to implement sticky table headers and not be aware of the <th> workaround.

  • Don't use table markup at all. Instead, use different elements (<div>s and whatnot) and other CSS layout methods to replicate the style of a table, but not locked out of using position: relative and creating position: sticky parent elements.
  • Use table elements, but totally remove all their styling defaults with new display values.

The first is dangerous because you aren't using semantic and accessible elements for the content to be read and navigated. The second is almost the same. You can go that route, but need to be really careful to re-apply semantic roles.

Anyway, none of that matters if you just stick (get it?!) to using a sticky value on those <th> elements.

See the Pen
Sticky Table Headers with CSS
by Chris Coyier (@chriscoyier)
on CodePen.

It's probably a bit weird to have table headers as a row in the middle of a table, but it's just illustrating the idea. I was imagining colored header bars separating players on different sports teams or something.

Anytime I think about data tables, I also think about how tricky it can be to make them responsive. Fortunately, there are a variety of ways, all depending on the best way to group and explore the data in them.

The post Position Sticky and Table Headers appeared first on CSS-Tricks.

How to Create a Sticky Image Effect with Three.js

If you recently browsed Awwwards or FWA you might have stumbled upon Ultranoir’s website. An all-round beautifully crafted website, with some amazing WebGL effects. One of which is a sticky effect for images in their project showcase. This tutorial is going to show how to recreate this special effect.

The same kind of effect can be seen on the amazing website of MakeReign.

Understanding the effect

When playing with the effect a couple of times we can make a very simple observation about the “stick”.

In either direction of the effect, the center always reaches its destination first, and the corners last. They go at the same speed, but start at different times.

With this simple observation we can extrapolate some of the things we need to do:

  1. Differentiate between the unsticky part of the image which is going to move normally and the sticky part of the image which is going to start with an offset. In this case, the corners are sticky and the center is unsticky.
  2. Sync the movements
    1. Move the unsticky part to the destination while not moving the sticky part.
    2. When the unsticky part reaches its destination, start moving the sticky part

Getting started

For this recreation we’ll be using three.js, and Popmotion’s Springs. But you can implement the same concepts using other libraries.

We’ll define a plane geometry with its height as the view height, and its width as 1.5 of the view width.

const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 10000);
const fovInRadians = (camera.fov * Math.PI) / 180;
// Camera aspect ratio is 1. The view width and height are equal.
const viewSize = Math.abs(camera.position.z * Math.tan(fovInRadians / 2) * 2);
const geometry = new THREE.PlaneBufferGeometry(viewSize *1.5,viewSize,60,60)

Then we’ll define a shader material with a few uniforms we are going to use later on:

  • u_progress Elapsed progress of the complete effect.
  • u_direction Direction to which u_progress is moving.
  • u_offset Largest z displacement
const material = new THREE.ShaderMaterial({
	uniforms: {
		// Progress of the effect
		u_progress: { type: "f", value: 0 },
		// In which direction is the effect going
		u_direction: { type: "f", value: 1 },
		u_waveIntensity: { type: "f", value: 0 }
	},
	vertexShader: vertex,
	fragmentShader: fragment,
	side: THREE.DoubleSide
});

We are going to focus on the vertex shader since the effect mostly happens in there. If you have an interest in learning about the things that happen in the fragment shader, check out the GitHub repo.

Into the stick

To find which parts are going to be sticky we are going to use a normalized distance from the center. Lower values mean less stickiness, and higher values mean more sticky. Since the corners are the farthest away from the center, they end up being most sticky.

Since our effect is happening in both directions, we are going to have it stick both ways. We have two separate variables:

  1. One that will stick to the front. Used when the effect is moving away from the screen.
  2. And a second one that will stick to the back. Used when the effect is moving towards the viewer.
uniform float u_progress;
uniform float u_direction;
uniform float u_offset;
uniform float u_time;
void main(){
	vec3 pos = position.xyz;
	float distance = length(uv.xy - 0.5 );
	float maxDistance = length(vec2(0.5,0.5));
	float normalizedDistance = distance/sizeDist;
	// Stick to the front
	float stickOutEffect = normalizedDistance ;
	// Stick to the back
	float stickInEffect = -normalizedDistance ;
	float stickEffect = mix(stickOutEffect,stickInEffect, u_direction);
	pos.z += stickEffect * u_offset;
	gl_Position =
	projectionMatrix *
	modelViewMatrix *
	vec4(pos, 1.0);
}

Depending on the direction, we are going to determine which parts are not going to move as much. Until we want them to stop being sticky and move normally.

The Animation

For the animation we have a few options to choose from:

  1. Tween and timelines: Definitely the easiest option. But we would have to reverse the animation if it ever gets interrupted which would look awkward.
  2. Springs and vertex-magic: A little bit more convoluted. But springs are made so they feel more fluid when interrupted or have their direction changed.

In our demo we are going to use Popmotion’s Springs. But tweens are also a valid option and ultranoir’s website actually uses them.

Note: When the progress is either 0 or 1, the direction will be instant since it doesn’t need to transform.

function onMouseDown(){
	...
	const directionSpring = spring({
		from: this.progress === 0 ? 0 : this.direction,
		to: 0,
		mass: 1,
		stiffness: 800,
		damping: 2000
	});
	const progressSpring = spring({
		from: this.progress,
		to: 1,
		mass: 5,
		stiffness: 350,
		damping: 500
	});
	parallel(directionSpring, progressSpring).start((values)=>{
		// update uniforms
	})
	...
}

function onMouseUp(){
	...
	const directionSpring = spring({
		from: this.progress === 1 ? 1 : this.direction,
		to: 1,
		mass: 1,
		stiffness: 800,
		damping: 2000
	});
	const progressSpring = spring({
		from: this.progress,
		to: 0,
		mass: 4,
		stiffness: 400,
		damping: 70,
		restDelta: 0.0001
	});
	parallel(directionSpring, progressSpring).start((values)=>{
		// update uniforms
	})
	...
}

And we are going to sequence the movements by moving through a wave using u_progress.

This wave is going to start at 0, reach 1 in the middle, and come back down to 0 in the end. Making it so the stick grows in the beginning and decreases in the end.

void main(){
	...
	float waveIn = u_progress*(1. / stick);
	float waveOut = -( u_progress - 1.) * (1./(1.-stick) );
	float stickProgress = min(waveIn, waveOut);
	pos.z += stickEffect * u_offset * stickProgress;
	gl_Position =
	projectionMatrix *
	modelViewMatrix *
	vec4(pos, 1.0);
}

Now, the last step is to move the plane back or forward as the stick is growing.

Since the stick grow starts in different values depending on the direction, we’ll also move and start the plane offset depending on the direction.

void main(){
	...
	float offsetIn = clamp(waveIn,0.,1.);
	// Invert waveOut to get the slope moving upwards to the right and move 1 the left
	float offsetOut = clamp(1.-waveOut,0.,1.);
	float offsetProgress = mix(offsetIn,offsetOut,u_direction);
	pos.z += stickEffect * u_offset * stickProgress - u_offset * offsetProgress;
	gl_Position =
	projectionMatrix *
	modelViewMatrix *
	vec4(pos, 1.0);
}

And here is the final result:

Conclusion

Simple effects like this one can make our experience look and feel great. But they only become amazing when complemented with other amazing details and effects. In this tutorial we’ve covered the core of the effect seen on ultranoir’s website, and we hope that it gave you some insight on the workings of such an animation. If you’d like to dive deeper into the complete demo, please feel free to explore the code.

We hope you enjoyed this tutorial, feel free to share your thoughts and questions in the comments!

How to Create a Sticky Image Effect with Three.js was written by Daniel Velasquez and published on Codrops.

Dealing with overflow and position: sticky;

Any overflow value other than visible and no height is the enemy of child elements with position: sticky;. It's like that element is ready to stick when the parent scrolls, but it never does because the height is unconstrained. Adding a fixed height can solve the issue, but that's not always desirable.

Dannie Vinther digs into a way of dealing with that. The end result is avoiding that situation all together by removing the element that wants to be sticky from the element that needs an overflow. But as soon as you do that, the elements no longer scroll together since they aren't siblings. The use case here is a table with sticky headers on vertical scrolling and allowing for horizontal scrolling as well. Dannie uses a script to sync the scroll positions.

Direct Link to ArticlePermalink

The post Dealing with overflow and position: sticky; appeared first on CSS-Tricks.

More Like position: tricky;

I rather like position: sticky;. It has practical use cases. I think of things like keeping a table of contents in a sidebar of a long article, but as a fairly simple implementation and without risk of overlapping things in awkward ways. But Elad Shechter is right here: it's not used that much — at least partially — and probably because it's a bit weird to understand.

I like how Elad explains it with a "Sticky Item" and a "Sticky Container." The container needs to be large enough that scrolling is relevant and for the stickiness to do anything at all.

There are other gotchas, too. I feel like every time I try position: sticky; in a real context, I have about a 30% chance of it working. There always seems to be some parent/child relationship thing that I can't quite work out to prevent overlaps. Or, there is some parent element with overflow: hidden;, which, for reasons unbeknownst to me, breaks this.

Direct Link to ArticlePermalink

The post More Like position: tricky; appeared first on CSS-Tricks.