I have the following design created by a Youtube tutorial about magic indicators:
But I want a smooth, not so steep curve as a continuation of the circle when selected instead of using element with small border-radius like in the original solution and I don’t know how to make it perfectly match the border of the circle. Code snippet is below. On the screenshot below I show with arrows the places where I make manual adjustments which don’t completely work. I’m looking for a CSS-only solution.
const indicator = document.querySelector("[data-indicator]")
document.addEventListener("click", e => {
let anchor
if (e.target.matches("a")) {
anchor = e.target
} else {
anchor = e.target.closest("a")
}
if (anchor != null) {
const allAnchors = [...document.querySelectorAll("a")]
const index = allAnchors.indexOf(anchor)
indicator.style.setProperty("--position", index)
document.querySelectorAll("a").forEach(elem => {
elem.classList.remove("active")
})
anchor.classList.add("active")
}
})
*, *::before, *::after {
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}
body {
background-color: var(--background-color);
color: white;
}
:root {
--icon-size: 2rem;
--indicator-spacing: calc(var(--icon-size) / 8);
--border-radius: calc(var(--icon-size) / 4);
--nav-item-padding: calc(var(--icon-size) / 2);
--background-color: #333;
}
.navbar-container {
background-color: white;
border-radius: var(--border-radius);
width: max-content;
margin: 0 auto;
margin-top: 10rem;
padding: 0 calc(var(--nav-item-padding) * 1.5);
}
.list {
display: flex;
margin: 0;
padding: 0;
list-style: none;
}
.list a {
color: #333;
text-decoration: none;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--nav-item-padding);
}
.list .text {
font-size: .8em;
opacity: 0;
pointer-events: none;
transition: 250ms ease-in-out;
position: absolute;
bottom: calc(.5 * var(--nav-item-padding));
transform: translateY(50%);
}
.list .icon {
position: relative;
transition: 250ms ease-in-out;
}
.list .icon svg {
fill: currentColor;
width: var(--icon-size);
height: var(--icon-size);
display: block;
}
.list .active .text {
pointer-events: all;
opacity: 1;
transform: translateY(0);
}
.list .active .icon {
transform: translateY(calc(-50% - var(--nav-item-padding)));
}
/* .list .active::before {
content: "";
box-sizing: content-box;
position: absolute;
width: var(--border-radius);
height: var(--border-radius);
background-color: white;
z-index: 1;
top: calc(-1 * var(--indicator-spacing));
left: calc(.2 * var(--indicator-spacing));
transform: translateX(-100%);
border-top-right-radius: 100%;
border-width: calc(var(--indicator-spacing));
border-color: var(--background-color);
border-style: solid;
border-bottom: none;
border-left: none;
}
.list .active::after {
content: "";
box-sizing: content-box;
position: absolute;
width: var(--border-radius);
height: var(--border-radius);
background-color: white;
z-index: 1;
top: calc(-1 * var(--indicator-spacing));
right: calc(.2 * var(--indicator-spacing));
transform: translateX(100%);
border-top-left-radius: 100%;
border-width: calc(var(--indicator-spacing));
border-color: var(--background-color);
border-style: solid;
border-bottom: none;
border-right: none;
} */
.list {
position: relative;
}
.indicator {
position: absolute;
left: calc(var(--position) * (var(--icon-size) + var(--nav-item-padding) * 2));
transition: 250ms ease-in-out;
}
.indicator::after,
.indicator::before {
content: "";
position: absolute;
border-radius: 100%;
}
.indicator::after {
background-color: hsl(100, 100%, 50%);
width: calc(var(--icon-size) * 2);
height: calc(var(--icon-size) * 2);
top: calc(-1 * var(--icon-size));
}
.indicator::before {
background-color: var(--background-color);
width: calc((var(--icon-size) + var(--indicator-spacing)) * 2);
height: calc((var(--icon-size) + var(--indicator-spacing)) * 2);
top: calc(-1 * var(--icon-size) - var(--indicator-spacing));
left: calc(-1 * var(--indicator-spacing));
}
.corners::before {
content: "";
box-sizing: content-box;
position: absolute;
width: var(--border-radius);
height: var(--border-radius);
background-color: white;
z-index: 1;
top: calc(-1 * var(--indicator-spacing));
left: calc(.2 * var(--indicator-spacing));
transform: translateX(-100%);
border-top-right-radius: 100%;
border-width: calc(var(--indicator-spacing));
border-color: var(--background-color);
border-style: solid;
border-bottom: none;
border-left: none;
}
.corners::after {
content: "";
box-sizing: content-box;
position: absolute;
width: var(--border-radius);
height: var(--border-radius);
background-color: white;
z-index: 1;
top: calc(-1 * var(--indicator-spacing));
left: calc(var(--icon-size) * 2 + -.2 * var(--indicator-spacing));
border-top-left-radius: 100%;
border-width: calc(var(--indicator-spacing));
border-color: var(--background-color);
border-style: solid;
border-bottom: none;
border-right: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<nav class="navbar-container">
<ul class="list">
<div style="--position: 0;" data-indicator class="indicator">
<div class="corners"></div>
</div>
<li><a href="#" class="active">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" ><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81l5-4.5M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z"/>
</svg>
</div>
<div class="text">Home</div>
</a></li>
<li><a href="#">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z"/></svg>
</div>
<div class="text">Account</div>
</a></li>
<li><a href="#">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
</div>
<div class="text">Messages</div>
</a></li>
<li><a href="#">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M14.12 4l1.83 2H20v12H4V6h4.05l1.83-2h4.24M15 2H9L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2zm-3 7c1.65 0 3 1.35 3 3s-1.35 3-3 3-3-1.35-3-3 1.35-3 3-3m0-2c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5z"/></svg>
</div>
<div class="text">Photos</div>
</a></li>
<li><a href="#">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.09-.16-.26-.25-.44-.25-.06 0-.12.01-.17.03l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.06-.02-.12-.03-.18-.03-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.09.16.26.25.44.25.06 0 .12-.01.17-.03l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
</div>
<div class="text">Settings</div>
</a></li>
</ul>
</nav>
</body>
</html>
2
Answers
I'm also posting my own solution using
clip-path
as suggested by @Paulie_D since it has its own benefits and drawbacks compared to @Brett Donald's solution.Here the drawback is I need to hardcode the point where the clipped content (watch for classes
.clip-wrapper
and.clip
) hits the circle (::before
of.indicator
). I had to also put someleft
to.clip-wrapper
but that's tightly coupled with the point of intersection so I count it as one situation of hardcoding.I manually composed the curve using cubic bezier curve and respectively the mirrored one on the right, but that was relatively easy to do.
The wrapper is necessary so I had the two colors when clipping - white for the clipped content and dark for the rest.
I would create an SVG shape to serve as the background of the
.indicator::before
pseudo-element. I would start with a circle bisected by a line, then draw some paths in the corners, then join it all together into a single filled path.You then need to encode the SVG for use in CSS. After setting it as the content of the
.indicator::before
pseudo-element, and deleting the.corners
element which is no longer needed, the final result looks like this:Snippet below to demonstrate.
It’s probably possible take the path out of this SVG and use it as a
clip-path
instead, but the end result would be the same.