Achieving a perfect curve with a specific circle radius using CSS

I came across a design tutorial on magic indicators from YouTube and created the following design: https://i.sstatic.net/IJjdCGWk.png

However, I am looking to achieve a smoother curve as a continuation of the circle when selected, instead of using elements with small border-radius like in the original solution. I'm struggling to make it perfectly match the border of the circle. The code snippet is provided below. I have indicated with arrows on the screenshot where I tried manual adjustments that didn't work well. I am seeking a CSS-only solution. https://i.sstatic.net/IYJ9UxoW.png

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 {
  ...
}

...

.indicator {
  position: absolute;
  left: calc(var(--position) * (var(--icon-size) + var(--nav-item-padding) * 2));
  transition: 250ms ease-in-out;
}

...
.corners::before {
  ...
}
LET'S KEEP THE HTML CONTENT UNCHANGED

Answer №1

To enhance the design, I would generate an SVG shape to act as the backdrop of the .indicator::before pseudo-element. I'd kick things off with a circle sliced by a line, add paths in the corners, and then merge everything into a single filled path.

https://i.sstatic.net/MBnlYuup.png

The subsequent step involves encoding the SVG for CSS application. After implementing it as the content of the .indicator::before pseudo-element, and removing the no longer necessary .corners element, the outcome appears similar to this:

https://i.sstatic.net/wirxVAvY.png

A snippet is provided below for demonstration purposes.

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;
}

/* Remaining CSS code omitted for brevity */

.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;
}

/* Further CSS styling details omitted for clarity */
<nav class="navbar-container">
  <ul class="list">
    <div style="--position: 0;" data-indicator class="indicator">
    </div>

    /* HTML markup for navigation icons removed for conciseness */
    
  </ul>
</nav>
It’s possible to extract the path from this SVG and utilize it as a clip-path, but the final outcome would be indistinguishable.

Answer №2

My solution utilizes the clip-path technique as recommended by @Paulie_D, which comes with its own set of advantages and disadvantages compared to @Brett Donald's approach.

The drawback here is that I have to manually specify the point where the clipped content intersects with the circle (look for classes .clip-wrapper and .clip) within the ::before pseudo-element of .indicator. Additionally, I had to apply some left property to .clip-wrapper, which is closely tied to the intersection point, so I consider it a form of hardcoding.

I constructed the curve manually using a cubic Bézier curve and its mirrored version on the right side, but this task was relatively straightforward.

The use of a wrapper element was necessary in order to display two different colors during clipping - white for the clipped region and dark for the rest.

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");
    }
});
*,
*::after,
::before {
  box-sizing: border-box;
  font-family: Arial, Helvetica, sans-serif;
}

:root {
  --icon-size: 2rem;
  --nav-item-padding: calc(var(--icon-size) / 2);
  --background-color: #333;
  --border-radius: calc(var(--icon-size) / 4);
  --indicator-spacing: calc(var(--icon-size) / 8);
}

body {
  background-color: var(--background-color);
  color: white;
}

.navbar-container {
  background-color: white;
  border-radius: 0.5rem;
  width: max-content;
  margin: 0 auto;
  margin-top: 10rem;
  padding: 0 calc(var(--nav-item-padding) * 4); // * 1.5
}

.list {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  text-decoration: 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: 0.8em;
  opacity: 0;
  pointer-events: none;
  transition: 500ms ease-in-out;
  position: absolute;
  bottom: calc(0.5 * var(--nav-item-padding));
  transform: translateY(50%);
}

.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 .icon {
  position: relative;
  transition: 250ms ease-in-out;
}

.list .active .icon {
  transform: translateY(calc(-50% - var(--nav-item-padding)));
}

/* .list .active::before {
  content: "";
  position: absolute;
  box-sizing: content-box;
  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;
}

...

.clip-wrapper.right .clip {
  clip-path: path("M 64 0 C 39 0, 29 0, 0 26.9 L 0 64 L 64 64 L 64 0z");
}
<html>
...
...
</html>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The drop-down arrow in the <select> element seems determined to stay within the container

I am currently exploring alternative solutions (without the use of JavaScript) to address a Firefox bug that prevents styling the dropdown arrow in select elements. Some sources suggest placing the select element within a container and adjusting the contai ...

The hyperlink appears to be malfunctioning

I've been working with a jQuery multilayer menu, but I encountered an issue with the link not being clickable. For reference: jQuery: JS Array Demo: When navigating through the menu on the demo site - "devices" -> "Mobile phones" -> "super s ...

When text is added to div elements, they are mysteriously pushed downward. This strange phenomenon occurs with inline-block elements

Why does the first element get pushed down when its child contains text? What causes inline-block to behave this way? Is there a way to align divs side-by-side while still allowing them to have children? For example, I want the grey box to contain a list ...

The Challenges of CSS Specificity in Multiple Browsers

I am using this as an opportunity to learn, so I appreciate if you can refrain from telling me what not to do and instead help me understand the concepts. What I am trying to achieve here is comprehension. Here is a sample of CSS code: input[type="file"] ...

Troubles arise with IE8 when using Position: fixed styling

Here is the CSS code I am using: background-color: White; border: 2px solid black; padding: 10px; position: fixed; right: 5px; top: 0; width: 250px; While this code works well in Chrome, Firefox, and Safari, I am facing issues with IE8. The position of t ...

CSS-only: Align items in flexbox column with equal width and centered position, without the need for a wrapper

https://i.stack.imgur.com/vAJv2.png This particular challenge involves creating a responsive mobile menu. In this design, the width of all children is determined by the longest child element. The CSS should ensure that the .index class spans the full wi ...

Input field, upon receiving focus, triggers a subtle shift in the position of the submit button

Thank you for the assistance, I believe this issue should be an easy fix. I have added a custom :focus style to my input that removes the default outline and adds a box shadow and border. However, when the input field is focused, the submit button located ...

Is it possible to resize the background-image style?

When using the regular img tag in HTML, you can specify the width and height, and the browser will scale the image. If the dimensions are not too different from the original, the result is usually decent. For example, you can scale the avatar image to 32x ...

The HTML iframe is displaying blank content

I'm trying to embed a webpage within another webpage using an iframe. I attempted to do so with this simple code: <iframe src="http://edition.cnn.com/" id="i_frame"></iframe> JSFIDDLE However, nothing is displaying. Any thoughts on why ...

Conceal/reveal div with initial partial visibility

http://jsfiddle.net/7RDBk/1/ At the moment, I am using slideUp and slideDown functions to hide and display a div. My goal is to have 25px of the div showing initially before expanding and going back to 25px when clicked. It seems like a simple task, but I ...

Is there a way for me to adjust the font size across the entire page?

Most CSS classes come with a fixed font-size value already set. For instance: font-size: 16px font-size: 14px etc. Is there a way to increase the font size of the entire page by 110%? For example, font-size: 16px -> 17.6 font-size: 14px -> 15.4 ...

Utilizing CSS to align all HTML form elements at the center

Is it possible to centrally align all form labels and input fields by using CSS? If so, what would be the approach for a form where the label text is displayed vertically above the input field? #fieldset label { display: block; } #fieldset input ...

Tips on modifying classes within specific sections of HTML tables

I attempted to create calendar-like tables, In my script, I am trying to handle events being registered. My goal is to change the classes of cells from 2 to 5 when they are clicked on, and change the cell colors from first to hovered to aqua. For this p ...

It's a mystery unraveling the source of CSS margins and padding

Could someone please help me analyze this webpage? I am trying to make the center of the page white up to the navigation bar, at the bottom of the page, and horizontally up to the shadows on both sides. I'm not sure where this extra padding is coming ...

Is it necessary to only override the monospaced font?

Can the monospace font in Angular Material be customized for just the <code>foo</code> blocks? I want to use a font where the number zero 0 looks distinct from the letter oh O. ...

use jquery to dynamically switch CSS class on navigation with animated arrows

Looking to update arrows to display a right arrow when the parent list item has the .active class. Here is the jQuery code: jQuery(function(){ initAccordion(); }); function initAccordion() { jQuery('.accordion').slideAccordion({ opener ...

Tips for integrating SASS from the Bulma package into an Angular application

I'm currently working on implementing Bulma into my project. The documentation suggests using NPM to obtain it. yarn add bulma Further in the documentation, there is an example provided with code snippets like the ones below. <link rel="styles ...

What is the best way to ensure the footer remains in an automatic position relative to the component's height?

I am struggling to position the footer component correctly at the end of my router-outlet. After trying various CSS properties, I managed to make the footer stay at the bottom but it acts like a fixed footer. However, I want the footer to adjust its positi ...

Enhance the appearance of the Vaadin Details Expand Icon by customizing the default CSS styling

For my expand/unexpand feature, I am utilizing Vaadin Details. Div contentDiv = new Div(); Label label = new Label("Expand"); Details details = new Details(label, contentDiv); The current output looks like this: https://i.sstatic.net/y8XQC.pn ...

Ensure that only one menu with a specific class is open at any given time

My goal is to ensure that both menus cannot be open simultaneously. The desired behavior is as follows: When one menu is already open and you click on another, the first one should automatically close. For a better understanding of what I am trying to achi ...