I’ve always liked tag clouds. I like the UX of seeing what tags are most popular on a website by seeing the relative font size of the tags, popular tags being bigger. They seem to have fallen out of fashion, though you do often see versions of them used in illustrations in tools like Wordle.
How difficult is it to make a tag cloud? Not very difficult at all. Let’s see!
Let’s start with the markup
For our HTML, we’re going to put each of our tags into a list, <ul class="tags"><ul>
. We’ll be injecting into that with JavaScript.
If your tag cloud is already in HTML, and you are just looking to do the relative font-size
thing, that’s good! Progressive enhancement! You should be able to adapt the JavaScript later on so it does just that part, but not necessarily building and injecting the tags themselves.
I have mocked out some JSON with a certain amount of articles tagged with each property. Let’s write some JavaScript to go grab that JSON feed and do three things.
First, we’ll create an <li>
from each entry for our list. Imagine the HTML, so far, is like this:
<ul class="tags">
<li>align-content</li>
<li>align-items</li>
<li>align-self</li>
<li>animation</li>
<li>...</li>
<li>z-index</li>
</ul>
Second, we’ll put the number of articles each property has in parentheses beside inside each list item. So now, the markup is like this:
<ul class="tags">
<li>align-content (2)</li>
<li>align-items (2)</li>
<li>align-self (2)</li>
<li>animation (9)</li>
<li>...</li>
<li>z-index (4)</li>
</ul>
Third, and last, we’ll create a link around each tag that goes to the correct place. This is where we can set the font-size
property for each item depending on how many articles that property is tagged with, so animation
that has 13 articles will be much bigger than background-color
which only has one article.
<li class="tag">
<a
class="tag__link"
href="https://example.com/tags/animation"
style="font-size: 5em">
animation (9)
</a>
</li>
The JavasScript part
Let’s have a look at the JavaScript to do this.
const dataURL =
"https://gist.githubusercontent.com/markconroy/536228ed416a551de8852b74615e55dd/raw/9b96c9049b10e7e18ee922b4caf9167acb4efdd6/tags.json";
const tags = document.querySelector(".tags");
const fragment = document.createDocumentFragment();
const maxFontSizeForTag = 6;
fetch(dataURL)
.then(function (res) {
return res.json();
})
.then(function (data) {
// 1. Create a new array from data
let orderedData = data.map((x) => x);
// 2. Order it by number of articles each tag has
orderedData.sort(function(a, b) {
return a.tagged_articles.length - b.tagged_articles.length;
});
orderedData = orderedData.reverse();
// 3. Get a value for the tag with the most articles
const highestValue = orderedData[0].tagged_articles.length;
// 4. Create a list item for each result from data.
data.forEach((result) => handleResult(result, highestValue));
// 5. Append the full list of tags to the tags element
tags.appendChild(tag);
});
The JavaScript above uses the Fetch API to fetch
the URL where tags.json
is hosted. Once it gets this data, it returns it as JSON. Here we seque into a new array called orderedData
(so we don’t mutate the original array), find the tag with the most articles. We’ll use this value later on in a font-scale so all other tags will have a font-size relative to it. Then, forEach
result in the response, we call a function I have named handleResult()
and pass the result
and the highestValue
to this function as a parameter. It also creates:
- a variable called
tags
which is what we will use to inject each list item that we create from the results, - a variable for a
fragment
to hold the result of each iteration of the loop, which we will later append to thetags
, and - a variable for the max font size, which we’ll use in our font scale later.
Next up, the handleResult(result)
function:
function handleResult(result, highestValue) {
const tag = document.createElement("li");
tag.classList.add("tag");
tag.innerHTML = `<a class="tag__link" href="${result.href}" style="font-size: ${result.tagged_articles.length * 1.25}em">${result.title} (${result.tagged_articles.length})</a>`;
// Append each tag to the fragment
fragment.appendChild(tag);
}
This is pretty simple function that creates a list element set to the variable named tag
and then adds a .tag
class to this list element. Once that’s created, it sets the innerHTML
of the list item to be a link and populates the values of that link with values from the JSON feed, such as a result.href
for the link to the tag. When each li
is created, it’s then added as a string to the fragment
, which we will later then append to the tags
variable. The most important item here is the inline style
tag that uses the number of articles—result.tagged_articles.length
—to set a relative font size using em
units for this list item. Later, we’ll change that value to a formula to use a basic font scale.
I find this JavaScript just a little bit ugly and hard on the eyes, so let’s create some variables and a simple font scale formula for each of our properties to tidy it up and make it easier to read.
function handleResult(result, highestValue) {
// Set our variables
const name = result.title;
const link = result.href;
const numberOfArticles = result.tagged_articles.length;
let fontSize = numberOfArticles / highestValue * maxFontSizeForTag;
fontSize = +fontSize.toFixed(2);
const fontSizeProperty = `${fontSize}em`;
// Create a list element for each tag and inline the font size
const tag = document.createElement("li");
tag.classList.add("tag");
tag.innerHTML = `<a class="tag__link" href="${link}" style="font-size: ${fontSizeProperty}">${name} (${numberOfArticles})</a>`;
// Append each tag to the fragment
fragment.appendChild(tag);
}
By setting some variables before we get into creating our HTML, the code is a lot easier to read. And it also makes our code a little bit more DRY, as we can use the numberOfArticles
variable in more than one place.
Once each of the tags has been returned in this .forEach
loop, they are collected together in the fragment
. After that, we use appendChild()
to add them to the tags
element. This means the DOM is manipulated only once, instead of being manipulated each time the loop runs, which is a nice performance boost if we happen to have a large number of tags.
Font scaling
What we have now will work fine for us, and we could start writing our CSS. However, our formula for the fontSize
variable means that the tag with the most articles (which is “flex” with 25) will be 6em (25 / 25 * 6 = 6), but the tags with only one article are going to be 1/25th the size of that (1 / 25 * 6 = 0.24), making the content unreadable. If we had a tag with 100 articles, the smaller tags would fare even worse (1 / 100 * 6 = 0.06).
To get around this, I have added a simple if
statement that if
the fontSize
that is returned is less than 1, set the fontSize
to 1. If not, keep it at its current size. Now, all the tags will be within a font scale of 1em to 6em, rounded off to two decimal places. To increase the size of the largest tag, just change the value of maxFontSizeForTag
. You can decide what works best for you based on the amount of content you are dealing with.
function handleResult(result, highestValue) {
// Set our variables
const numberOfArticles = result.tagged_articles.length;
const name = result.title;
const link = result.href;
let fontSize = numberOfArticles / highestValue * maxFontSizeForTag;
fontSize = +fontSize.toFixed(2);
// Make sure our font size will be at least 1em
if (fontSize <= 1) {
fontSize = 1;
} else {
fontSize = fontSize;
}
const fontSizeProperty = `${fontSize}em`;
// Then, create a list element for each tag and inline the font size.
tag = document.createElement("li");
tag.classList.add("tag");
tag.innerHTML = `<a class="tag__link" href="${link}" style="font-size: ${fontSizeProperty}">${name} (${numberOfArticles})</a>`;
// Append each tag to the fragment
fragment.appendChild(tag);
}
Now the CSS!
We’re using flexbox for our layout since each of the tags can be of varying width. We then center-align them with justify-content: center
, and remove the list bullets.
.tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 960px;
margin: auto;
padding: 2rem 0 1rem;
list-style: none;
border: 2px solid white;
border-radius: 5px;
}
We’ll also use flexbox for the individual tags. This allows us to vertically align them with align-items: center
since they will have varying heights based on their font sizes.
.tag {
display: flex;
align-items: center;
margin: 0.25rem 1rem;
}
Each link in the tag cloud has a small bit of padding, just to allow it to be clickable slightly outside of its strict dimensions.
.tag__link {
padding: 5px 5px 0;
transition: 0.3s;
text-decoration: none;
}
I find this is handy on small screens especially for people who might find it harder to tap on links. The initial text-decoration
is removed as I think we can assume each item of text in the tag cloud is a link and so a special decoration is not needed for them.
I’ll just drop in some colors to style things up a bit more:
.tag:nth-of-type(4n+1) .tag__link {
color: #ffd560;
}
.tag:nth-of-type(4n+2) .tag__link {
color: #ee4266;
}
.tag:nth-of-type(4n+3) .tag__link {
color: #9e88f7;
}
.tag:nth-of-type(4n+4) .tag__link {
color: #54d0ff;
}
The color scheme for this was stolen directly from Chris’ blogroll, where every fourth tag starting at tag one is yellow, every fourth tag starting at tag two is red, every fourth tag starting at tag three is purple. and every fourth tag starting at tag four is blue.
We then set the focus and hover states for each link:
.tag:nth-of-type(4n+1) .tag__link:focus,
.tag:nth-of-type(4n+1) .tag__link:hover {
box-shadow: inset 0 -1.3em 0 0 #ffd560;
}
.tag:nth-of-type(4n+2) .tag__link:focus,
.tag:nth-of-type(4n+2) .tag__link:hover {
box-shadow: inset 0 -1.3em 0 0 #ee4266;
}
.tag:nth-of-type(4n+3) .tag__link:focus,
.tag:nth-of-type(4n+3) .tag__link:hover {
box-shadow: inset 0 -1.3em 0 0 #9e88f7;
}
.tag:nth-of-type(4n+4) .tag__link:focus,
.tag:nth-of-type(4n+4) .tag__link:hover {
box-shadow: inset 0 -1.3em 0 0 #54d0ff;
}
I could probably have created a custom variable for the colors at this stage—like --yellow: #ffd560
, etc.—but decided to go with the longhand approach for IE 11 support. I love the box-shadow
hover effect. It’s a very small amount of code to achieve something much more visually-appealing than a standard underline or bottom-border. Using em
units here means we have decent control over how large the shadow would be in relation to the text it needed to cover.
OK, let’s top this off by setting every tag link to be black on hover:
.tag:nth-of-type(4n+1) .tag__link:focus,
.tag:nth-of-type(4n+1) .tag__link:hover,
.tag:nth-of-type(4n+2) .tag__link:focus,
.tag:nth-of-type(4n+2) .tag__link:hover,
.tag:nth-of-type(4n+3) .tag__link:focus,
.tag:nth-of-type(4n+3) .tag__link:hover,
.tag:nth-of-type(4n+4) .tag__link:focus,
.tag:nth-of-type(4n+4) .tag__link:hover {
color: black;
}
And we’re done! Here’s the final result:
Nice tutorial, but I have on question.
In the following part of the code, is the else even required?
What would happen if it was simplified to just the following?
Well spotted, Wiktor. Looks like the else may not be required there.
I love tag clouds, I thought I was the only one left!
Instead of a static minimum value for the font size, I use a
log()
function of the number of elements. I find it better lowers the gap between a lot of not much used tags and a few used a lot.Here’s my result:
https://nicolas-hoizey.com/tags/
The color is also based on the number of items.
Here’s where I use the
log()
function:https://github.com/nhoizey/nicolas-hoizey.com/blob/master/src/_11ty/getTags.js#L33-L39
And the Nunjucks macro that uses the result, in my Eleventy build:
https://github.com/nhoizey/nicolas-hoizey.com/blob/master/src/_includes/macros/tagsCloud.njk
HTH
Hi Nicolas,
That looks very neat, well done. Thanks for sharing that info.
I like that inset effect but unfortunately it has some problems if there’s any word wrapping going on. I assume the 1.3em is chosen somewhat arbitrarily to work alright with the given line height?
At any rate it causes problems – you get an unfortunately illegible effect with the top half of the word entirely obscured on phones and narrower screens. Ideas about how to fix this would be great!
Hi Mg,
Nice find. I hadn’t noticed that myself.
I guess you could remove the box-shadow and replace it with a linear-gradient, that might work:
background-image: linear-gradient(to top, #ee4266 90%, transparent 90%)
}
Is there a way to change the repetitive listing of the same words in the Json file?
},
Becomes:
},
or something like that?
Having the words repeat over and over increases the file size tremendously.
Thanks!
Frank