cssuser-interface

Css3 donut menu


Hi I would like to implement a donut menu like this one http://dribbble.com/shots/610433-Wheel-Nav I know there is a simple way to do a donuts in css3.

.doughnut { 
    border: 50px solid #f00;
    border-radius: 100px;
    height:100px;
    width:100px;
}

BUT of course this only make the donut without any elements inside of it. I'm wonder if it's anyway to do it only with css3, any ideas of how to start? if is not possible with only css i will jump into javascript area...


Solution

  • 2024 solution, live demo

    Result

    screenshot of 2024 demo result

    How

    Now using CSS grid, variables, clip-path, mask, SVG filters (for background graininess + inner shadows).

    2013 solution, preserved for web history

    My attempt to reproduce that image with CSS:

    Result:

    result

    HTML:

    <ul class='menu circ-menu'>
      <li class='menu-item'>
        <a href='#'>☊</a>
      </li>
      <li class='menu-item selected'>
        <a href='#'>☁☀</a>
        <ul class='menu submenu'>
          <li><a href='#'>☂</a></li><!--
          --><li><a href='#'>☁</a></li><!--
          --><li><a href='#'>☃</a></li>
        </ul>
      </li>
      <li class='menu-item'>
        <a href='#'>✦</a>
      </li>
    </ul>
    

    CSS:

    .menu { padding: 0; list-style: none; }
    .menu a {
      display: block;
      color: #666561;
      font: 900 2em/3.2 monospace;
      text-decoration: none;
      text-shadow: 0 1px white;
    }
    .circ-menu {
      overflow: hidden;
      position: relative;
      margin: 1em auto;
      padding: 5em 0 0;
      width: 20em; height: 10em;
      box-shadow: 0 .5em .5em -.5em;
    }
    .menu-item {
      overflow: hidden;
      position: absolute;
      z-index: 0;
      left: -50%; bottom: 0;
      width: 20em; height: 20em;
      transform-origin: 100% 100%;
    }
    
    /* === style the menu items (slices) === */
    
    /* three slices making up half a circle mean that a slice is going to have a central_angle = 60deg */
    .menu-item:first-child {
      transform: skewX(30deg) /* 90deg - central_angle */;
    }
    .menu-item:nth-child(2) {
      /* rotate by the value of the central angle multiplied with how many slices are before */
      transform: 
        rotate(60deg) /* 60deg = 1*central angle */
        skewX(30deg) /* 90deg - central_angle */;
    }
    .menu-item:last-child {
      transform: 
        rotate(120deg) /* 120deg = 2*central angle */
        skewX(30deg) /* 90deg - central_angle */;
    }
    
    /* === contents of the menu items === */
    .menu-item > * {
      position: absolute;
      top: 55%; left: 55%;
      width: 90%; height: 90%;
      text-align: center;
    }
    .menu-item > a {
      border-radius: 50%;
      box-shadow: 0 0 0 .2em #aaa497, 
        0 0 .5em .2em black;
      transform:
        skewX(-30deg) /* unskew */
        rotate(-60deg);
      background: #f8f4ef;
      background: 
        radial-gradient(transparent 39%, #f7f3ee 40%);
    }
    .selected > a { z-index: 1; }
    .selected > a, .menu a:hover {
      color: #e96429;
    }
    .selected > a, .menu-item > a:hover {
      box-shadow: 0 0 0 .2em #e96429, 
        0 0 .5em .2em black;
      background: 
        linear-gradient(60deg, #e96429 31%, rgba(247, 243, 238, 0) 33%),
        linear-gradient(-60deg, #e96429 31%, rgba(247, 243, 238, 0) 33%),
        radial-gradient(transparent 39%, #f7f3ee 40%);
      background-size: 100% 26%,  100% 26%, 100% 100%;
    }
    
    .submenu {
      z-index: 0;
      transform: 
        skewX(-30deg) /* unskew */
        rotate(-60deg)
        translateY(-5.25em);
    }
    .submenu li {
      display: inline-block;
      position: relative;
      border-top: solid 1px #666561;
      border-bottom: solid 1px #666561;
      width: 3.2em; height: 3.2em;
      background: #f7f3ee;
    }
    .submenu li:before {
      position: absolute;
      bottom: 0;
      width: 100%; height: .2em;
      background: #666561;
      content: '';
    }
    .submenu a {
      line-height: 2
    }
    .submenu li:first-child {
      border-radius: .3em 0 0 .3em;
      border-left: solid 1px #666561;
    }
    .submenu:after {
      position: absolute;
      z-index: -1;
      top: 2.5em; left: 50%;
      margin: 0 -.6em;
      width: 1.2em; height: 1.2em;
      transform: rotate(-30deg) skewX(30deg);
      background: #666561;
      content: '';
    }
    .submenu li:last-child {
      border-radius: 0 .3em .3em 0;
      border-right: solid 1px #666561;
    }
    
    .circ-menu:before, .circ-menu:after {
      position: absolute;
      z-index: 1;
      bottom: -7.071em; left: 14.645%;
      width: 14.142em; height: 14.142em;
      border-radius: 50%;
      content: '';
    }
    .circ-menu:before {
      bottom: -5em; left: 5em;
      width: 10em; height: 10em;
      box-shadow: inset 0 0 .75em black, 
        0 0 .5em .2em #f7f3ee;
    }
    .circ-menu:after {
      bottom: -1em; left: 9em;
      width: 2em; height: 2em;
      box-shadow: 0 0 .4em dimgrey, 
        0 0 0 .75em #e27447, 
        0 0 .4em .75em dimgrey, 
        0 0 0 2em #f7f3ee, 
        0 0 .4em 2em dimgrey;
      background: #f7a480;
    }
    

    The idea is pretty simple. You start with a list structure, just like you would do for every menu.

    <ul class='menu-circ'>
      <li class='menu-item'><a href='#'>boo</a></li>
      <!-- the other list items -->
    </ul>
    

    Explaining the basic idea

    You give the container .menu-circ position: relative and you absolutely position its children (the menu items) such that one of their corners is at the centre (of .menu-circ). Then you set the transform-origin for .menu-item to be in that corner.

    In this case, I've chosen the bottom right corner to be at the centre (transform-origin: right bottom is equivalent to transform-origin: 100% 100%), but it doesn't really matter, you can choose whichever corner you want to put at the centre and set the transform-origin there.

    You then need to decide on the value of the central angle for each slice that a menu item forms. In this case, it was easy - there were three slices for half a circle, half a circle means 180deg, so assuming that I want 3 equal slices, the central angle for each is 180deg/3 = 60deg.

    Having decided on that, you now need to make the sides that meet at the centre of the parent (right and bottom sides in this case) form an angle of 60deg. You do that by applying a skew transform, the skew angle being 90deg-60deg = 30deg:

    transform: skewX(30deg);
    

    But this still leaves all the menu items in the position of the first one. So for all the menu items but the first one, you also need to rotate the by the value of the central angle times how many items there are before. This means that you'll have:

    .menu-item:first-child { transform: skewX(30deg); } /* no items before */
    .menu-item:nth-child(2) { transform: rotate(60deg) /* 1*60deg */ skewX(30deg); }
    .menu-item:nth-child(3) { transform: rotate(120deg) /* 2*60deg */ skewX(30deg); } 
    

    This positions all the menu items where they should be, but now their contents are skewed. So you need to "unskew" them. By that, I mean that you apply a skew transform with the opposite angle. In this case, unskewing means applying skewX(-30deg).

    Still, the angle for the content is not right. You need to rotate it by half the central angle minus 90deg in this case. This means by 60deg/2 - 90deg = 30deg - 90deg = -60deg.

    The content (link in this case) should also be absolutely positioned such that its centre is at the point where you've set the transform-origin for its parent (.menu-item) you can give it any size you want, but if you want it to be circular, then you'll have to give it equal width and height and make sure that they aren't more than twice those of its parent (.menu-item).

    Finally, you set overflow: hidden on .menu-item and you have a pie menu. If you want to cover the central part, you use a pseudo-element on the menu (.menu-circ).