Sticky Bullet Points


Today I saw a really interesting effect with some bullet points. What it showed was a box that would scroll with the page, and stack with the next one as it went, giving a 3D stacking effect. I really liked the idea of this and wanted to implement it myself to see how it worked.

To see what I mean, here is a video of the effect in action:

Design

For my version of the effect, I am going to keep it as simple as possible with only HTML and CSS. I am also going to use a CSS border rather than an SVG for the boxes so I can focus on the functionality, rather than the design.

The box I am using is a simple div with a border. I think a 50 pixel square should do for the example.


The only addition to this is to give it a slight 3d effect by rotating and skewing it a little. This will make the stacks look a little nicer too.


The complete CSS for this box is as follows:

.box {
    --bullet-size: 50px;
    height: var(--bullet-size);
    width: var(--bullet-size);
    border: 2px solid rgb(var(--body--accent));
    border-radius: 2px;
    transform: rotate(45deg) skew(-20deg, -20deg);
}

Stacking

The simple part of this design is actually stacking the boxes. CSS has always had the ability to specify which element should be placed over another when they overlap using the z-index property.

For this I am using the sibling-index() and sibling-count() functions. These are quite new to CSS and, at the time of writing, are not available in all browsers, but they are supported in the latest versions of Chrome, Edge, and Opera.

These functions can be used together to apply styles based on the number of boxes in the list.

  • sibling-index - returns the index of the current element in the list of sibling elements, starting at 1.
  • sibling-count - returns the total number of sibling elements in the list.

To calculate the z-index, we can simply subtract the sibling index from the sibling count. This gives the topmost element the highest z-index value, allowing it to overlap the others.

z-index: calc(sibling-count() - sibling-index());

Older browser support

If I really needed to support all browsers I could instead apply this style per child. It is a little more work and must be changed if the number of boxes changes, but it is still simple enough.

For example, with three boxes I could do the following:

.box:nth-child(1) {
    z-index: 3;
}
.box:nth-child(2) {
    z-index: 2;
}
.box:nth-child(3) {
    z-index: 1;
}

Positioning

Although it may seem relatively simple to make this effect, there are a few tricky concepts to allow this on multiple screen sizes and with multiple boxes. There is also the requirement to have the box and content align next to each other on the page.

I think it is useful to understand the DOM element requirements for this effect.

HTML Structure

The general structure I am going to use is two columns, one for the content and one for the boxes that will scroll with the page. The general layout looks like this:

Box 1

Box 2

Box 3

Content 1

Content 2

Content 3


There are three parts to this structure:

  1. The root element
  2. The column wrappers
  3. The content and boxes

Boxes

In order to get the boxes to follow the page down as you scroll we need to use the position: sticky property. This will stick the element to the specified position while it is within the viewport, and there is room for it to do so within its containing block.

The key part here is that the containing block is the bounding box for the sticky element. This means that the ancestor must be the full height of the scroll area required for the sticky effect to work. All the sticky boxes should be within this ancestor element.

Content

The content next to the boxes should scroll normally with the page, so does not need any special positioning. It should, however, have each box aligned to the related content. This worked in my example box model above, but what happens if the content is larger than the box height?

Box 1

Box 2

Box 3

Content 1 with more text to simulate larger content. The idea is that this box is going to cause a misalignment between the stacks and the content.

Content 2

Content 3



Well, that’s not right. There is now a major misalignment between the boxes and the content. This is because the bullets and content do not affect each other at the moment.

To fix this, the new subgrid option can be used. This property lets a grid inherit the row and column structure of its parent grid, allowing the boxes and content to be aligned based on the same row structure.

Setting the column wrappers to have subgrid enabled will let both the boxes and content be aligned to the same rows.

.column-wrapper {
    display: grid;
    grid-template-rows: subgrid;
    grid-template-columns: subgrid;
}

With that in place, the boxes and content are all aligned to the same row.

Box 1

Box 2

Box 3

Content 1 with more text to simulate larger content. The idea is that this box is going to cause a misalignment between the stacks and the content.

Content 2

Content 3



Boxes scrolling with the page

Now that the position of the bullet points is correct, and the stacking z-index is figured out, the final part is to make the boxes scroll with the page.

This is simple now that there is a common ancestor element that is the full height of the scroll area. We can simply apply position: sticky to the boxes when the parent has position: relative.

Lastly in order to make it look a little better there is some tweaking of the top and margin-bottom properties to make the boxes stack with a little gap between them.

.sticky-box {
    position: sticky;
    top: var(--scroll-start);
    margin-bottom: calc(var(--bullet-offset) * (sibling-count() - sibling-index()) + 25px);
}

Box 1

Box 2

Box 3

Content 1 with more text to simulate larger content. The idea is that this box is going to cause a misalignment between the stacks and the content.

Content 2

Content 3



Adding my styling

All the core work is done now, so the final step is to add my styled box.


Here is the first piece of content. The start of this box should line up nicely with the first bullet point.

Now on to the second. Wow, look at those boxes stack!

Now there are three! This seems to be working pretty well.

Fourth and final - the bullets should stop here.

Hopefully you find this helpful to create something interesting with your own content. I have added the full working example below if you want to have a play with it.



Want to read more? Check out more posts below!