Menus with a sliding marker

(06-22-2014).

This is a JavaScript-free solution to create a "sliding" marker next to or behind links in vertical or horizontal menus. Something called JQuery MagicLine Navigation on CSS-TRICKS.

TL;DR:

With an additional sliding bullet on codepen, and a horizontal version at the bottom of this article.

Make sure to read the update section.

The Markup

This solution relies on a structural hack as we use the last item in the list to create the marker.

<ul class="menu">
    <li>
        <a href="#0">Link 1</a>
    </li>
    <li>
        <a href="#0">Link 2</a>
    </li>
    <li class="selected">
        <a tabindex="-1" href="#0">Link 3</a>
    </li>
    <li>
        <a href="#0">Link 4</a>
    </li>
    <li>
        <a href="#0">Link 5</a>
    </li>
    <li>
        <a href="#0">Link 6</a>
    </li>
    <li>
        <a href="#0">Link 7</a>
    </li>
    <li>
        <a href="#0">Link 8</a>
    </li>
    <li>
        <a href="#0">Link 9</a>
    </li>
    <li><!-- this is truly ugly --></li>
</ul>

The Styles

The logic relies on the general sibling combinator (~). We pair this combinator with :last-child to target the last sibling in the list whenever the mouse hovers over an item in that list (see the magic).

Note: make sure to add vendor prefixes to the declarations below.

Step by step

We reset default styles on the menu and its items.

.menu,
.menu li {
    margin: 0;
    padding: 0;
    list-style: none;
}

position:relative makes the menu the containing-block for the marker we’ll be using. overflow:hidden will hide the marker outside the list. Feel free to change the width or border of the menu.

.menu {
    position: relative;
    overflow: hidden;
    width: 125px;
    border-right: 1px solid rgba(255, 99, 71, 0.4);
}

These 2 declarations will vertically center the text in the list items. You may want to add white-space:nowrap to prevent any wrapping as the line-height would create too much space between lines of text. The positioning of the marker will be based on this height value.

Note that we use the child combinator (>) to make sure we only style top list items (as with links).

.menu > li {
    line-height: 2em;
    height: 2em;
}

We style the links as block so they spread across the list. The padding creates some space on each side of those links.

.menu > li > a {
    display: block;
    padding: 0 2em;
}

This is our marker. We “highjack” the last item in the list to make it slide along the right border of the list. The top offset value is just enough to position it outside the list.

.menu > li:last-child {
    position: absolute;
    height: 2em;
    top: -2em;
    right: 0;
    width: .3em;
    background: rgba(255, 99, 71, 0.4);
    transition: transform .3s;
}

This other marker is to “tag” the selected (active) item (self-referencing link). position:relative is there to make sure the pseudo-element positions itself in relation to this list item rather than the menu.

Note that we style this item as if it was not a link.

.menu .selected {
    position: relative;
    pointer-events: none;
    cursor: default;
}

This is styled the same way we styled the last-item in the list earlier. Same height and same width values. The background color has a different opacity which creates a subtle effect when the mouse hovers over other items in the list.

.menu .selected::after {
    content: "";
    position: absolute;
    background: rgba(255, 99, 71, 0.6);
    height: 2em;
    width: .3em;
    top: 0;
    right: 0;
}

The magic

This is how we make things work: we use the general sibling combinator to target the last item in the list and position it relative to the item being hovered over. :nth-child(1) is the first item in the list; it is just 2em below the “hidden” marker (which is styled with top:-2em;). Moving the marker down by 2em will bring it to the same level as the first item.

The use of !important here is to increase the weight of the rule to make sure it overwrites the one containing the .selected class.

.menu li:nth-child(1):hover ~ li:last-child {
    transform:translateY(2em) !important;
}
.menu li:nth-child(1).selected ~ li:last-child {
    transform:translateY(2em);
}

Same as above, but with an increment of 2em with each item.

.menu li:nth-child(2):hover ~ li:last-child {
    transform:translateY(4em) !important;
}
.menu li:nth-child(2).selected ~ li:last-child {
    transform:translateY(4em);
}
.menu li:nth-child(3):hover ~ li:last-child {
    transform:translateY(6em) !important;
}
.menu li:nth-child(3).selected ~ li:last-child {
    transform:translateY(6em);
}
.menu li:nth-child(4):hover ~ li:last-child {
    transform:translateY(8em) !important;
}
.menu li:nth-child(4).selected ~ li:last-child {
    transform:translateY(8em);
}
.menu li:nth-child(5):hover ~ li:last-child {
    transform:translateY(10em) !important;
}
.menu li:nth-child(5).selected ~ li:last-child {
    transform:translateY(10em);
}
.menu li:nth-child(6):hover ~ li:last-child {
    transform:translateY(12em) !important;
}
.menu li:nth-child(6).selected ~ li:last-child {
    transform:translateY(12em);
}
.menu li:nth-child(7):hover ~ li:last-child {
    transform:translateY(14em) !important;
}
.menu li:nth-child(7).selected ~ li:last-child {
    transform:translateY(14em);
}
.menu li:nth-child(8):hover ~ li:last-child {
    transform:translateY(16em) !important;
}
.menu li:nth-child(8).selected ~ li:last-child {
    transform:translateY(16em);
}
.menu li:nth-child(9):hover ~ li:last-child {
    transform:translateY(18em) !important;
}
.menu li:nth-child(9).selected ~ li:last-child {
    transform:translateY(18em);
}

What about horizontal lists?

We can follow the same principle to create a sliding box behind the items in a horizontal list. The catch is that we need to deal with explict widths (see this codepen.)

Update

After posting this solution on Twitter, I learned that a few people had already explored this idea:

Note that Vincent’s solution relies on a pseudo-element instead of an empty list-item. If you style your selected/active element with position:relative - as we do here - you’ll need to change Vincent’s styling as shown in his other demo. It is not very flexible but you may want to consider this approach for “static” menus (menus in which the number of items does not vary) as relying on structural hacks should be avoided whenever possible.