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 preprocessor), 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