A few months ago I created a CSS “diamond” mesh navigation; this time, I thought I’d do a hexagonal mesh with random highlights to spark viewer interest.

While image outlines in squares, circles and even octagons are relatively easy to create in CSS, hexagons are a little more challenging, so I decided to use SVG and clip-path to achieve the effect.

The Markup

The HTML is fairly straightforward: I’ll give just three examples of the image tiles, as there’s a lot of repetition:

<div id="honeycomb">
  <a href="#" class="active">
    <img src="ferns.jpg" srcset="ferns-2x.jpg 2x" alt="Ferns">
  </a>
  <a href="#">
    <img src="canyon.jpg" srcset="canyon-2x.jpg 2x" alt="Canyon Wall">
  </a>
  <a href="#" class="active">
    <img src="agave-cactus.jpg" srcset="agave-cactus-2x.jpg 2x" alt="Agave cactus">
  </a>
  …
</div>

Links that are active (i.e. they would actually go somewhere when clicked) are given a class of the same name. srcset is used for optimised file size; alternatively, a large sprite map could be used, although the advantages of spriting will be less effective as HTTP2 is increasingly adopted over time.

To clip the images in non-Webkit-dervied browsers I’ll add SVG markup to the page. The clipPath is in a 1 × 1 box:

<svg id="clippy">
    <defs>
        <clipPath id="hexagon" clipPathUnits="objectBoundingBox">
            <polygon points=".25,.934 0,.5 .25, .068 .75, .068 1, .5 .75, .934" />
        </clipPath>
    </defs>
</svg>

To reference the SVG, and create the equivalent CSS, I’ll use clip-path in an embedded style sheet.

The CSS

The opening CSS includes basic styles for the <body> and the SVG (reduced in size so that it doesn’t take any actual space on the page: this does not affect its clipping ability) and sizing the container for the hexagons, while not allowing them to overflow:

body { 
  margin: 0;
  background: #000;
}
#clippy { 
  width: 0; height: 0;
}
#honeycomb { 
    position: relative;
    padding-top: 50%;
    overflow: hidden;
}

The links are given absolute positioning inside the relative container (meaning that they will positioned relative to it, not the <body>. The CSS clip-path is provided with a vendor prefix; note that this is not extended to the transition, which will be used in a moment.

#honeycomb a { 
    position: absolute;
    width: 38%;
    top: -4%;
    left: 1%;
    clip-path: url(#hexagon);
    -webkit-clip-path: polygon(25% 93.4%, 0% 50%, 25% 6.8%, 75% 6.8%, 100% 50%, 75% 93.4%);
    clip-path: polygon(25% 93.4%, 0% 50%, 25% 6.8%, 75% 6.8%, 100% 50%, 75% 93.4%);
    opacity: .5;
    transition: .5s opacity;
    }
#honeycomb img {
    width: 100%;
    height: auto;
}

The JavaScript randomize function will apply a current class to the link it highlights; visually, this will be the same as the mouse or touch hover on the same link, so the selectors are grouped:

#honeycomb a.active:hover, #honeycomb a.current {
    opacity: 1;
}

At the same time, we want links that do not lead anywhere to not have this hover effect, so the :not selector is used to ensure that links that do not have a class of .active don’t respond to interaction events:

#honeycomb a:not(.active) {
  pointer-events: none;
}

The JavaScript

If the user doesn’t take any action, I want the links to be randomly highlighted to lead them to some interaction:

var active = document.getElementsByClassName("active");
    
function highlight() {
    var randomhex = Math.floor(Math.random() * active.length);
    active[randomhex].classList.add("current");
    var fadeInterval = window.setTimeout(function() { 
        active[randomhex].classList.remove("current") 
    },  2000);
}
    
var highlightInterval = window.setInterval(function() { highlight() }, 3000);

active gathers the links that lead somewhere into a JavaScript array; the highlight function (called every three seconds) selects a random number between 0 and 2 (stored as randomhex), highlights the associated link from the active array, and then fades it out after two seconds.

Cleanup

The hexagon links need to be positioned, otherwise they will all appear exactly on top of each other. I did this using inline styles, judging the results by eye:

<div id="honeycomb">
    …
  <a href="#" style="left: 60%; top: 66%;">
        <img src="golden-forest.jpg" srcset="golden-forest-2x.jpg 2x" alt="Golden Forest">
    </a>
  <a href="#" style="top: -38.5%; left: 30.5%">
    <img src="weeds.jpg" srcset="weeds-2x.jpg 2x" alt="Weeds">
  </a>
  …
</div>

For greater accuracy and efficiency, you could formally calculate the position of the hexagons (perhaps expressing it with CSS calc or using a ), applying the results with small, shared classes.

One remaining problem is that the user could move over a link while the JavaScript continues to highlight other links. While there are a number of solutions to this issue, I opted for a simple CSS override:

#honeycomb:hover a.current {
    opacity: 0.5;
}

Meaning: if the user’s cursor is active anywhere in the honeycomb element, set anything with a class of current back to its default opacity. The result is that - while the JavaScript will continue to run - the user won’t see any changes if their cursor is over one of the active links.

Photos by Klaus Burmeister, Alan English, Tom Hall, Theophilos Papadopoulos, Peter & Utne Grahlmann, John & Fish and James Marvin Phelps, licensed under Creative Commons.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/xwRXBv