skip to Main Content

I’m trying to create a circular graph using only CSS and HTML, where each segment has a border with rounded ends at both the start and end. However, I’m struggling with positioning the ::before and ::after pseudo-elements to correctly mark the start and end of each segment’s border.

Here’s what I’m currently working with:

  • I’m using conic-gradient to fill the circle based on a percentage (--value).
  • I want to add small rounded borders (12px diameter) at both the start (::before) and end (::after) of the border for each segment.

I’m using calc() along with cos() and sin() to try to calculate the position of the ::after pseudo-element based on the percentage of the circle filled (--value). However, it doesn’t seem to be working correctly.

How can I accurately position the ::after pseudo-element at the end of the filled segment on the circular graph, ensuring that it aligns with the edge of the filled arc?

Thank you for your help!

This is a simplified version of my CSS:

.circular {
        &__graph {
        grid-area: b;
        max-width: 100%;
        width: 100%;
        margin-top: 36px;
        justify-content: center;
        display: flex;
        flex-direction: column;
        padding-left: 65px;

        &-chart{
            display: flex;
            width: 100%;
            height: 500px;
            } 
    }

    &__circles{
        display: flex;
        align-content: center;
        justify-content: center;
        align-items: center;
        position: relative;
        width: 100%;
    }

&__dial-circle {
        --size: 320px;
        --thickness: 12px;
        width: var(--size);
        height: var(--size);
        border-radius: 60%;
        background: conic-gradient(var(--color, #1e90ff) calc(var(--value)* 1%), #F9F9F9 0);
        mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), white calc(100% - var(--thickness) + 1px));
        position: absolute;
        //transform: rotate(180deg);


        &::before, &::after {
            content: ' ';
            width: 12px;
            height: 12px;
            position: absolute;
            background: var(--color, #1e90ff);
            left: calc(50% - 7px);
            top: 0;
            border-radius: 50%;
        }

        &::after{
            border: 1px solid red;
            --angle: calc(var(--value) * 3.6);
            --radius: calc((var(--size) / 2) - var(--thickness));

            left: calc(50% + var(--radius) * cos(var(--angle) * (pi / 180)) - 6px);
            top: calc(50% + var(--radius) * sin(var(--angle) * (pi / 180)) - 6px);

        }
    }
}

Here’s my HTML for this example:

<div class="circular__graph-chart">
    <div class="circular__circles">
        <div class="circular__dial-circle" style="--color: rgb(39, 63, 82);--value: 79.5;--size: 314px;"></div>
        <div class="circular__dial-circle" style="--color: rgb(36, 91, 120);--value: 72.8;--size: 282px"></div>
        <div class="circular__dial-circle" style="--color: rgb(105, 129, 142);--value: 86;--size: 250px"></div>
        <div class="circular__dial-circle" style="--color: rgb(189, 200, 201);--value: 68.2;--size: 217px"></div>
        <div class="circular__dial-circle" style="--color: rgb(231, 195, 184);--value: 79.1;--size: 185px"></div>
        <div class="circular__dial-circle" style="--color: rgb(195, 175, 180);--value: 86.8;--size: 152px"></div>
    </div>
</div>

Here’s a live example: https://codepen.io/JonJonCS/pen/BaXopEE

I tried using the cos() and sin() functions in calc() to position the ::after element at the end of the filled segment. I expected the ::after element to appear exactly at the edge of the filled arc, but instead, it’s positioned incorrectly and doesn’t align with the edge of the segment. I was hoping the red circle would mark the exact end of the border.

2

Answers


  1. Frankly. mere CSS and HTML is underpowered for this.

    Use an SVG and leverage pathLength and stroke-linecap: round

    svg {
      height: 80vh;
      margin: 10vh auto;
      border: 1px solid red;
      display: block;
      transform: rotate(-90deg);
    }
    
    svg circle {
      stroke-width: 5;
      fill: transparent;
    }
    
    #outer {
      stroke: lightgrey;
      animation: value 5s linear infinite;
      stroke-linecap: round
    }
    
    #inner {
      stroke: blue;
      stroke-width: 10;
      animation: value 5s linear infinite;
      stroke-linecap: round;
    }
    
    @keyframes value {
      0% {
        stroke-dasharray: 0 100;
      }
      100% {
        stroke-dasharray: 100 100;
      }
    }
    <svg viewbox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
      <circle id="outer" cx="60" cy="60" r="50" pathLength="100" />
    
      <circle id="inner" pathLength="100" cx="60" cy="60" r="30" />
    </svg>
    Login or Signup to reply.
  2. With visual debugging, I noticed that all "end caps" were off by 90 degrees. Also, their radius was incorrect.

    After adjusting --angle by minus 90 (degrees) and --radius by minus half (instead of all) its thickness in .circular__dial-circle::after, the end cap was properly aligned:

    .circular__graph {
      grid-area: b;
      max-width: 100%;
      width: 100%;
      margin-top: 36px;
      justify-content: center;
      display: flex;
      flex-direction: column;
      padding-left: 65px;
    }
    
    .circular__graph-chart {
      display: flex;
      width: 100%;
      height: 500px;
    }
    
    .circular__circles {
      display: flex;
      align-content: center;
      justify-content: center;
      align-items: center;
      position: relative;
      width: 100%;
    }
    
    .circular__dial-circle {
      --size: 320px;
      --thickness: 12px;
      width: var(--size);
      height: var(--size);
      border-radius: 60%;
      background: conic-gradient(var(--color, #1e90ff) calc(var(--value)* 1%), #F9F9F9 0);
      mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), white calc(100% - var(--thickness) + 1px));
      position: absolute;
      /*transform: rotate(180deg);*/
    }
    
    .circular__dial-circle::before,
    .circular__dial-circle::after {
      content: ' ';
      width: 12px;
      height: 12px;
      position: absolute;
      background: var(--color, #1e90ff);
      left: calc(50% - 7px);
      top: 0;
      border-radius: 50%;
    }
    
    .circular__dial-circle::after {
      /*border: 1px solid red;*/ /* Commented out for now */
      --angle: calc(var(--value) * 3.6 - 90); /* Added: "- 90" */
      --radius: calc((var(--size) / 2) - var(--thickness) / 2); /* Added: The last "/ 2" */
    
      left: calc(50% + var(--radius) * cos(var(--angle) * (pi / 180)) - 6px);
      top: calc(50% + var(--radius) * sin(var(--angle) * (pi / 180)) - 6px);
    }
    <div class="circular__graph-chart">
        <div class="circular__circles">
            <div class="circular__dial-circle" style="--color: rgb(39, 63, 82);--value: 79.5;--size: 314px;"></div>
            <div class="circular__dial-circle" style="--color: rgb(36, 91, 120);--value: 72.8;--size: 282px"></div>
            <div class="circular__dial-circle" style="--color: rgb(105, 129, 142);--value: 86;--size: 250px"></div>
            <div class="circular__dial-circle" style="--color: rgb(189, 200, 201);--value: 68.2;--size: 217px"></div>
            <div class="circular__dial-circle" style="--color: rgb(231, 195, 184);--value: 79.1;--size: 185px"></div>
            <div class="circular__dial-circle" style="--color: rgb(195, 175, 180);--value: 86.8;--size: 152px"></div>
        </div>
    </div>

    Personally, I find using polar coordinates (angle and radius) with rotate() more intuitive:

    .circular__graph {
      grid-area: b;
      max-width: 100%;
      width: 100%;
      margin-top: 36px;
      justify-content: center;
      display: flex;
      flex-direction: column;
      padding-left: 65px;
    }
    
    .circular__graph-chart {
      display: flex;
      width: 100%;
      height: 500px;
    }
    
    .circular__circles {
      display: flex;
      align-content: center;
      justify-content: center;
      align-items: center;
      position: relative;
      width: 100%;
    }
    
    .circular__dial-circle {
      --size: 320px;
      --thickness: 12px;
      width: var(--size);
      height: var(--size);
      border-radius: 60%;
      background: conic-gradient(var(--color, #1e90ff) calc(var(--value)* 1%), #F9F9F9 0);
      mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), white calc(100% - var(--thickness) + 1px));
      position: absolute;
      /*transform: rotate(180deg);*/
    }
    
    .circular__dial-circle::before,
    .circular__dial-circle::after {
      content: ' ';
      width: 12px;
      height: 12px;
      position: absolute;
      background: var(--color, #1e90ff);
      left: calc(50% - 7px);
      top: 0;
      border-radius: 50%;
    }
    
    .circular__dial-circle::after {
      /*border: 1px solid red;*/ /* Commented out for now */
      --angle: calc(var(--value) * 3.6deg); /* Same as your `* 3.6`, but with unit */
      --radius: calc((var(--size) - var(--thickness)) / 2); /* Added: The last "/ 2" */
    
      left: 50%;
      top: 50%;
      
      transform:
        translate(-50%, -50%) /* With `left`, `top`: Place centered */
        rotate(var(--angle)) /* Use `--angle` */
        translateY(calc(-1 * var(--radius))); /* Move outwards (along rotation, back to initial) */
    }
    <div class="circular__graph-chart">
        <div class="circular__circles">
            <div class="circular__dial-circle" style="--color: rgb(39, 63, 82);--value: 79.5;--size: 314px;"></div>
            <div class="circular__dial-circle" style="--color: rgb(36, 91, 120);--value: 72.8;--size: 282px"></div>
            <div class="circular__dial-circle" style="--color: rgb(105, 129, 142);--value: 86;--size: 250px"></div>
            <div class="circular__dial-circle" style="--color: rgb(189, 200, 201);--value: 68.2;--size: 217px"></div>
            <div class="circular__dial-circle" style="--color: rgb(231, 195, 184);--value: 79.1;--size: 185px"></div>
            <div class="circular__dial-circle" style="--color: rgb(195, 175, 180);--value: 86.8;--size: 152px"></div>
        </div>
    </div>
    1. Center element and its transform origin (with left, top and translate(-50%, -50%)).
    2. Rotate as desired (angle of polar coordinate).
    3. Offset as desired (radius of polar coordinate).
    4. Reverse rotation (so the final element isn’t rotated).

    Here, step 4 is redundant because a circle is similar to its rotation.


    Graphics such as these are usually easier in SVG. Take a look at Paulie_D’s answer.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search