112

How can I position several <img> elements into a circle around another and have those elements all be clickable links as well? I want it to look like the picture below, but I have no idea how to achieve that effect.

Desired Result

Is this even possible?

FatalKeystroke
  • 2,882
  • 7
  • 23
  • 35

9 Answers9

223

2020 solution

Here's a more modern solution I use these days.

I start off by generating the HTML starting from an array of images. Whether the HTML is generated using PHP, JS, some HTML preprocessor, whatever... this matters less as the basic idea behind is the same.

Here's the Pug code that would do this:

//- start with an array of images, described by url and alt text
- let imgs = [
-   {
-       src: 'image_url.jpg', 
-       alt: 'image alt text'
-   } /* and so on, add more images here */
- ];
- let n_imgs = imgs.length;
- let has_mid = 1; /* 0 if there's no item in the middle, 1 otherwise */
- let m = n_imgs - has_mid; /* how many are ON the circle */
- let tan = Math.tan(Math.PI/m); /* tangent of half the base angle */

.container(style=`--m: ${m}; --tan: ${+tan.toFixed(2)}`)
    - for(let i = 0; i < n_imgs; i++)
        a(href='#' style=i - has_mid >= 0 ? `--i: ${i}` : null)
          img(src=imgs[i].src alt=imgs[i].alt)

The generated HTML looks as follows (and yes, you can write the HTML manually too, but it's going to be a pain to make changes afterwards):

<div class="container" style="--m: 8; --tan: 0.41">
  <a href='#'>
    <img src="image_mid.jpg" alt="alt text"/>
  </a>
  <a style="--i: 1">
    <img src="first_img_on_circle.jpg" alt="alt text"/>
  </a>
  <!-- the rest of those placed on the circle -->
</div>

In the CSS, we decide on a size for the images, let's say 8em. The --m items are positioned on a circle and it's if they're in the middle of the edges of a polygon of --m edges, all of which are tangent to the circle.

If you have a hard time picturing that, you can play with this interactive demo which constructs the incircle and circumcircle for various polygons whose number of edges you pick by dragging the slider.

incircle and circumcircle of a hexagon

This tells us that the size of the container must be twice the radius of the circle plus twice half the size of the images.

We don't yet know the radius, but we can compute it if we know the number of edges (and therefore the tangent of half the base angle, precomputed and set as a custom property --tan) and the polygon edge. We probably want the polygon edge to be a least the size of the images, but how much we leave on the sides is arbitrary. Let's say we have half the image size on each side, so the polygon edge is twice the image size. This gives us the following CSS:

.container {
  --d: 6.5em; /* image size */
  --rel: 1; /* how much extra space we want between images, 1 = one image size */
  --r: calc(.5*(1 + var(--rel))*var(--d)/var(--tan)); /* circle radius */
  --s: calc(2*var(--r) + var(--d)); /* container size */
  position: relative;
  width: var(--s); height: var(--s);
  background: silver /* to show images perfectly fit in container */
}

.container a {
  position: absolute;
  top: 50%; left: 50%;
  margin: calc(-.5*var(--d));
  width: var(--d); height: var(--d);
  --az: calc(var(--i)*1turn/var(--m));
  transform: 
    rotate(var(--az)) 
    translate(var(--r))
    rotate(calc(-1*var(--az)))
}

img { max-width: 100% }

See the old solution for an explanation of how the transform chain works.

This way, adding or removing an image from the array of images automatically arranges the new number of images on a circle such that they're equally spaced out and also adjusts the size of the container. You can test this in this demo.


OLD solution (preserved for historical reasons)

Yes, it is very much possible and very simple using just CSS. You just need to have clear in mind the angles at which you want the links with the images (I've added a piece of code at the end just for showing the angles whenever you hover one of them).

You first need a wrapper. I set its diameter to be 24em (width: 24em; height: 24em; does that), you can set it to whatever you want. You give it position: relative;.

You then position your links with the images in the center of that wrapper, both horizontally and vertically. You do that by setting position: absolute; and then top: 50%; left: 50%; and margin: -2em; (where 2em is half the width of the link with the image, which I've set to be 4em - again, you can change it to whatever you wish, but don't forget to change the margin in that case).

You then decide on the angles at which you want to have your links with the images and you add a class deg{desired_angle} (for example deg0 or deg45 or whatever). Then for each such class you apply chained CSS transforms, like this:

.deg{desired_angle} {
   transform: rotate({desired_angle}) translate(12em) rotate(-{desired_angle});
}

where you replace {desired_angle} with 0, 45, and so on...

The first rotate transform rotates the object and its axes, the translate transform translates the object along the rotated X axis and the second rotate transform brings back the object into position.

The advantage of this method is that it is flexible. You can add new images at different angles without altering the current structure.

CODE SNIPPET

    .circle-container {
        position: relative;
        width: 24em;
        height: 24em;
        padding: 2.8em;
        /*2.8em = 2em*1.4 (2em = half the width of a link with img, 1.4 = sqrt(2))*/
        border: dashed 1px;
        border-radius: 50%;
        margin: 1.75em auto 0;
    }
    .circle-container a {
        display: block;
        position: absolute;
        top: 50%; left: 50%;
        width: 4em; height: 4em;
        margin: -2em;
    }
    .circle-container img { display: block; width: 100%; }
    .deg0 { transform: translate(12em); } /* 12em = half the width of the wrapper */
    .deg45 { transform: rotate(45deg) translate(12em) rotate(-45deg); }
    .deg135 { transform: rotate(135deg) translate(12em) rotate(-135deg); }
    .deg180 { transform: translate(-12em); }
    .deg225 { transform: rotate(225deg) translate(12em) rotate(-225deg); }
    .deg315 { transform: rotate(315deg) translate(12em) rotate(-315deg); }
    <div class='circle-container'>
        <a href='#' class='center'><img src='image.jpg'></a>
        <a href='#' class='deg0'><img src='image.jpg'></a>
        <a href='#' class='deg45'><img src='image.jpg'></a>
        <a href='#' class='deg135'><img src='image.jpg'></a>
        <a href='#' class='deg180'><img src='image.jpg'></a>
        <a href='#' class='deg225'><img src='image.jpg'></a>
        <a href='#' class='deg315'><img src='image.jpg'></a>
    </div>

Also, you could further simplify the HTML by using background images for the links instead of using img tags.


EDIT: example with fallback for IE8 and older (tested in IE8 and IE7)

Ana
  • 35,599
  • 6
  • 80
  • 131
  • 1
    Nice, but what will people see when accessing from devices/browsers with no CSS Transform support? – gkond Oct 10 '12 at 14:56
  • 1
    The only desktop browsers that don't support CSS transforms are IE8 and older. For those, this can be emulated using IE matrix filter transforms. As for mobile browsers, Opera Mini is the only one not supporting CSS transforms and I really wouldn't use something that's so space wasting on a small screen anyway. – Ana Oct 10 '12 at 15:21
  • I've added an example with fallback for IE8 and older. – Ana Oct 10 '12 at 16:29
  • 1
    When I saw the demo I scrolled down cause I knew it would be you who answer a question like that. Well done @Ana. Where the hell do you blog? – Ahmad Alfy Oct 23 '13 at 12:55
  • 6
    @Ana that's awesome, used your CSS to make a generic example for n items ,if interested.http://jsfiddle.net/sajjansarkar/zgcgq8cg/ – Sajjan Sarkar Jan 02 '15 at 21:42
  • Its really useful @Ana, but I am trying to draw the smaller circle inside the large circle, if would be great if you can provide some suggestion or fiddle link to draw circle inside circle (around 3-4 circles). – Anil Kumar Pandey Apr 20 '16 at 17:26
  • 3
    @Ana very cool! You inspired me to create a dynamic version - https://jsfiddle.net/skwidbreth/q59s90oy/ – skwidbreth Jan 10 '17 at 19:50
  • @Ana realy good example i am trying this same concept in mobile platform i am facing issue in responsive in various mobile – Mohan Gopi Aug 31 '17 at 15:09
  • Came here by the link on https://css-tricks.com/snippets/sass/placing-items-circle/. For your info and regrettable for all readers the links to the demos do not work anymore. – David Jul 18 '20 at 05:42
  • @David None of my pre-2013 links work. I deleted my GitHub account some years back and since everything was there... Not a big loss, the web has evolved and 5+ year old solutions are not exactly the way to go today. Here's a better, more current solution https://twitter.com/anatudor/status/1270756411346243587 – Ana Jul 18 '20 at 06:52
  • @Ana thanks a lot for the update, being glad that you also offered your new solution directly in the answer ;-) – David Jul 19 '20 at 15:29
  • is it possible to add a slight rotation to icons around circle? – DoctorHe Jun 20 '22 at 14:27
  • what can i do if the width of the container is specified instead of images? – DoctorHe Jun 20 '22 at 14:30
19

Using the solution proposed by @Ana:

transform: rotate(${angle}deg) translate(${radius}px) rotate(-${angle}deg)

I created the following jsFiddle that places circles dynamically using plain JavaScript (jQuery version also available).

The way it works is rather simple:

document.querySelectorAll( '.ciclegraph' ).forEach( ( ciclegraph )=>{
  let circles = ciclegraph.querySelectorAll( '.circle' )
  let angle = 360-90, dangle = 360 / circles.length
  for( let i = 0; i < circles.length; ++i ){
    let circle = circles[i]
    angle += dangle
    circle.style.transform = `rotate(${angle}deg) translate(${ciclegraph.clientWidth / 2}px) rotate(-${angle}deg)`
  }
})
.ciclegraph {
  position: relative;
  width: 500px;
  height: 500px;
  margin: calc(100px / 2 + 0px);
}

.ciclegraph:before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  border: 2px solid teal;
  width: calc( 100% - 2px * 2);
  height: calc( 100% - 2px * 2 );
  border-radius: 50%;
}

.ciclegraph .circle {
  position: absolute;
  top: 50%; left: 50%;
  width: 100px;
  height: 100px;
  margin: calc( -100px / 2 );
  background: teal;
  border-radius: 50%;
}
<div class="ciclegraph">
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
</div>
Itay Grudev
  • 7,055
  • 4
  • 54
  • 86
18

Here is the easy solution without absolute positioning:

.container .row {
  margin: 20px;
  text-align: center;
}

.container .row img {
  margin: 0 20px;
}
<div class="container">
  <div class="row">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
  </div>
  <div class="row">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
  </div>
  <div class="row">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
  </div>
</div>

http://jsfiddle.net/mD6H6/

Vadim Ovchinnikov
  • 13,327
  • 5
  • 62
  • 90
gkond
  • 3,818
  • 2
  • 20
  • 27
15

Building off @Ana's excellent answer, I created this dynamic version that allows you to add and remove elements from the DOM and maintain proportionate spacing between the elements - check out my fiddle: https://jsfiddle.net/skwidbreth/q59s90oy/

var list = $("#list");

var updateLayout = function(listItems) {
  for (var i = 0; i < listItems.length; i++) {
    var offsetAngle = 360 / listItems.length;
    var rotateAngle = offsetAngle * i;
    $(listItems[i]).css("transform", "rotate(" + rotateAngle + "deg) translate(0, -200px) rotate(-" + rotateAngle + "deg)")
  };
};

$(document).on("click", "#add-item", function() {
  var listItem = $("<li class='list-item'>Things go here<button class='remove-item'>Remove</button></li>");
  list.append(listItem);
  var listItems = $(".list-item");
  updateLayout(listItems);

});

$(document).on("click", ".remove-item", function() {
  $(this).parent().remove();
  var listItems = $(".list-item");
  updateLayout(listItems);
});
#list {
  background-color: blue;
  height: 400px;
  width: 400px;
  border-radius: 50%;
  position: relative;
}

.list-item {
  list-style: none;
  background-color: red;
  height: 50px;
  width: 50px;
  position: absolute;
  top: 50%;
  left: 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

<ul id="list"></ul>
<button id="add-item">Add item</button>
Vadim Ovchinnikov
  • 13,327
  • 5
  • 62
  • 90
skwidbreth
  • 7,888
  • 11
  • 58
  • 105
  • 1
    Worked great, and I'd upvote more if I could. One problem I had is that if I changed 360 to anything else (I wanted a half-circle), things got out of whack. I traced it to the declaration of rotate angle and changed it to this `var rotateAngle = zero_start + (offsetAngle * i || 0);` I also added a variable for zero_start so if you want to start at point 270 rather than 0, or something similar. https://jsfiddle.net/q59s90oy/13/. Lastly I changed the css for list items to use negative margins. **Seriously though, thanks for sharing the work, helped out a lot.** – Regular Jo Oct 16 '17 at 04:16
  • That's rad, glad you were able to tweak it as needed. Nice variation! – skwidbreth Oct 16 '17 at 15:46
  • 1
    Yo this is a pretty epic spiral effect https://i.imgur.com/1VrubKC.png – Ethan Jan 04 '19 at 06:33
  • @Ethan Ha ha yeah! I love doing that! I thought it could make a cool art piece. – skwidbreth Jan 04 '19 at 13:58
  • This is great, but when making it start a 0 and do 360 the added divs move inward in horizontal center left and right sides (drift inward). The top and bottom added divs center are aligned with the list circle. – jdog Oct 04 '21 at 22:52
  • @jdog to fix it you can change the top and the left (css , list-item) to ```top: calc(50% - 25px); left: calc(50% - 25px)``` the 25px is half of the item size (in this case 50px) – Elna Haim Dec 28 '21 at 09:50
14

Here is a version I made in React from the examples here.

CodeSandbox Example

import React, { useRef, useEffect } from "react";

import "./styles.css";

export default function App() {
  const graph = useRef(null);

  useEffect(() => {
    const ciclegraph = graph.current;
    const circleElements = ciclegraph.childNodes;

    let angle = 360 - 90;
    let dangle = 360 / circleElements.length;

    for (let i = 0; i < circleElements.length; i++) {
      let circle = circleElements[i];
      angle += dangle;
      circle.style.transform = `rotate(${angle}deg) translate(${ciclegraph.clientWidth /
        2}px) rotate(-${angle}deg)`;
    }
  }, []);

  return (
    <div className="App">
      <div className="ciclegraph" ref={graph}>
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
      </div>
    </div>
  );
}
br3ntor
  • 141
  • 1
  • 3
8

You can certainly do it with pure css or use JavaScript. My suggestion:

  • If you already know that the images number will never change just calculate your styles and go with plain css (pros: better performances, very reliable)

  • If the number can vary either dynamically in your app or just may vary in the future go with a Js solution (pros: more future-proof)

I had a similar job to do, so I created a script and open sourced it here on Github for anyone who might need it. It just accepts some configuration values and simply outputs the CSS code you need.

If you want to go for the Js solution here's a simple pointer that can be useful to you. Using this html as a starting point being #box the container and .dot the image/div in the middle you want all your other images around:

Starting html:

    <div id="box">
      <div class="dot"></div>
      <img src="my-img.jpg">
      <!-- all the other images you need-->
    </div>

Starting Css:

     #box{
      width: 400px;
      height: 400px;
      position: relative;
      border-radius: 100%;
      border: 1px solid teal;
    }

    .dot{
        position: absolute;
        border-radius: 100%;
        width: 40px;
        height: 40px;
        left: 50%;
        top: 50%;
        margin-left: -20px;
        margin-top: -20px;
        background: rebeccapurple;
    }
    img{
      width: 40px;
      height: 40px;
      position: absolute;
    }

You can create a quick function along these lines:

    var circle = document.getElementById('box'),
        imgs = document.getElementsByTagName('img'),
        total = imgs.length,
        coords = {},
        diam, radius1, radius2, imgW;
    
    // get circle diameter
    // getBoundingClientRect outputs the actual px AFTER transform
    //      using getComputedStyle does the job as we want
    diam = parseInt( window.getComputedStyle(circle).getPropertyValue('width') ),
    radius = diam/2,
    imgW = imgs[0].getBoundingClientRect().width,
    // get the dimensions of the inner circle we want the images to align to
    radius2 = radius - imgW
    
    var i,
        alpha = Math.PI / 2,
        len = imgs.length,
        corner = 2 * Math.PI / total;
    
    // loop over the images and assign the correct css props
    for ( i = 0 ; i < total; i++ ){
    
      imgs[i].style.left = parseInt( ( radius - imgW / 2 ) + ( radius2 * Math.cos( alpha ) ) ) + 'px'
      imgs[i].style.top =  parseInt( ( radius - imgW / 2 ) - ( radius2 * Math.sin( alpha ) ) ) + 'px'
    
      alpha = alpha - corner;
    }

You can see a live example here

Aurelio
  • 24,702
  • 9
  • 60
  • 63
5

There is no way to magically place clickable items in a circle around another element with CSS. The way how I would do this is by using a container with position:relative;. And then place all the elements with position:absolute; and using top and left to target it's place.

Even though you haven't placed in your tags it might be best to use jQuery / javascript for this.

First step is placing your center image perfectly in the center of the container using position:relative;.

#centerImage {
  position:absolute;
  top:50%;
  left:50%;
  width:200px;
  height:200px;
  margin: -100px 0 0 -100px;
}

After that you can place the other elements around it by using an offset() of the centerImage minus the offset() of the container. Giving you the exact top and left of the image.

var left = $('#centerImage').offset().left - $('#centerImage').parent().offset().left;
var top = $('#centerImage').offset().top - $('#centerImage').parent().offset().top;

$('#surroundingElement1').css({
  'left': left - 50,
  'top': top - 50 
});

$('#surroundingElement2').css({
  'left': left - 50,
  'top': top 
});

$('#surroundingElement3').css({
  'left': left - 50,
  'top': top + 50 
});

What I've done here is placing the elements relative to the centerImage. Hope this helps.

Sem
  • 4,477
  • 4
  • 33
  • 52
1

You could do it like this: fiddle

Don't mind the positioning, its a quick example

Mark
  • 6,762
  • 1
  • 33
  • 50
1

The first step is to have 6 long columnar boxes:

enter image description here

The second step is to use position: absolute and move them all into the middle of your container:

enter image description here

And now rotate them around the pivot point located at the bottom center. Use :nth-child to vary rotation angles:

div {
    transform-origin: bottom center;
    @for $n from 0 through 7 {
      &:nth-child(#{$n}) {
        rotate: (360deg / 6) * $n;
      }
}

enter image description here

Now all you have to do is to locate your images at the far end of every column, and compensate the rotation with an anti-rotation :)

Full source:

<div class="flower">
  <div class="petal">1</div>
  <div class="petal">2</div>
  <div class="petal">3</div>
  <div class="petal">4</div>
  <div class="petal">5</div>
  <div class="petal">6</div>
</div>
.flower {
  width: 300px;
  height: 300px;
  
  // We need a relative position 
  // so that children can have  "position:abolute"
  position: relative;
  
  .petal {
    // Make sure petals are visible
    border: 1px solid #999;
  
    // Position them all in one point
    position: absolute; top: 0; left: 50%;
    display: inline-block;
    width: 30px; height: 150px;
    
    // Rotation
    transform-origin: bottom center;
    @for $n from 0 through 7 {
      &:nth-child(#{$n}) {
        // Petal rotation
        $angle: (360deg / 6) * $n;
        rotate: $angle;
        // Icon anti-rotation
        .icon { rotate: -$angle; }
      }
    }
  }
}

See CodePen

kolypto
  • 31,774
  • 17
  • 105
  • 99