Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom element appears in the DOM, we wanna load the corresponding implementation if it’s not available yet. The browser then takes care of upgrading such elements from there on out.
Chances are you won’t actually need all this; there’s usually a simpler approach. Used deliberately, the techniques shown here might still be a useful addition to your toolset.
For consistency, we want our auto-loader to be a custom element as well — which also means we can easily configure it via HTML. But first, let’s identify those unresolved custom elements, step by step:
class AutoLoader extends HTMLElement { connectedCallback() { let scope = this.parentNode; this.discover(scope); } } customElements.define("ce-autoloader", AutoLoader);
Assuming we’ve loaded this module up-front (using async is ideal), we can drop a <ce-autoloader> element into the <body> of our document. That will immediately start the discovery process for all child elements of <body>, which now constitutes our root element. We could limit discovery to a subtree of our document by adding <ce-autoloader> to the respective container element instead — indeed, we might even have multiple instances for different subtrees.
Of course, we still have to implement that discover method (as part of the AutoLoader class above):
discover(scope) { let candidates = [scope, ...scope.querySelectorAll("*")]; for(let el of candidates) { let tag = el.localName; if(tag.includes("-") && !customElements.get(tag)) { this.load(tag); } } }
Here we check our root element along with every single descendant (*). If it’s a custom element — as indicated by hyphenated tags — but not yet upgraded, we’ll attempt to load the corresponding definition. Querying the DOM that way might be expensive, so we should be a little careful. We can alleviate load on the main thread by deferring this work:
let defer = window.requestIdleCallback || requestAnimationFrame; class AutoLoader extends HTMLElement { connectedCallback() { let scope = this.parentNode; defer(() => { this.discover(scope); }); } // ... }
Now we can move on to implementing the missing load method to dynamically inject a <script> element:
load(tag) { let el = document.createElement("script"); let res = new Promise((resolve, reject) => { el.addEventListener("load", ev => { resolve(null); }); el.addEventListener("error", ev => { reject(new Error("failed to locate custom-element definition")); }); }); el.src = this.elementURL(tag); document.head.appendChild(el); return res; } elementURL(tag) { return `$ {this.rootDir}/$ {tag}.js`; }
Note the hard-coded convention in elementURL. The src attribute’s URL assumes there’s a directory where all custom element definitions reside (e.g. <my-widget> → /components/my-widget.js). We could come up with more elaborate strategies, but this is good enough for our purposes. Relegating this URL to a separate method allows for project-specific subclassing when needed:
Either way, note that we’re relying on this.rootDir. This is where the aforementioned configurability comes in. Let’s add a corresponding getter:
get rootDir() { let uri = this.getAttribute("root-dir"); if(!uri) { throw new Error("cannot auto-load custom elements: missing `root-dir`"); } if(uri.endsWith("/")) { // remove trailing slash return uri.substring(0, uri.length - 1); } return uri; }
You might be thinking of observedAttributes now, but that doesn’t really make things easier. Plus updating root-dir at runtime seems like something we’re never going to need.
Now we can — and must — configure our elements directory: <ce-autoloader root-dir="/components">.
With this, our auto-loader can do its job. Except it only works once, for elements that already exist when the auto-loader is initialized. We’ll probably want to account for dynamically added elements as well. That’s where MutationObserver comes into play:
This way, the browser notifies us whenever a new element appears in the DOM — or rather, our respective subtree — which we then use to restart the discovery process. (You might argue we’re re-inventing custom elements here, and you’d be kind of correct.)
Our auto-loader is now fully functional. Future enhancements might look into potential race conditions and investigate optimizations. But chances are this is good enough for most scenarios. Let me know in the comments if you have a different approach and we can compare notes!
We’ve accomplished a bunch of stuff in this series! We created a custom WordPress block that fetches data from an external API and renders it on the front end. Then we took that work and extended it so the data also renders directly in the WordPress block editor. After that, we created a settings UI for the block using components from the WordPress InspectorControls package.
There’s one last bit for us to cover and that’s saving the settings options. If we recall from the last article, we’re technically able to “save” our selections in the block settings UI, but those aren’t actually stored anywhere. If we make a few selections, save them, then return to the post, the settings are completely reset.
Let’s close the loop and save those settings so they persist the next time we edit a post that contains our custom block!
We’re working with an API that provides us with soccer football team ranking and we’re using it to fetch for displaying rankings based on country, league, and season. We can create new attributes for each of those like this:
Next, we need to set the attributes from LeagueSettings.js. Whenever a ComboboxControl is updated in our settings UI, we need to set the attributes using the setAttributes() method. This was more straightfoward when we were only working with one data endpoint. But now that we have multiple inputs, it’s a little more involved.
This is how I am going to organize it. I am going to create a new object in LeagueSettings.js that follows the structure of the settings attributes and their values.
In each of the handle______Change(), I am going to create a setLocalAttributes() that has an argument that clones and overwrites the previous localSettings object with the new country, league, and season values. This is done using the help of the spread operator.
// LeagueSettings.js function handleCountryChange(value) { // Initial code setLocalAttributes({ ...localSettings, country: value }); // Rest of the code } function handleLeagueChange(value) { // Initial code setLocalAttributes({ ...localSettings, league: value }); // Rest of the code } function handleSeasonChange(value) { // Initial code setLocalAttributes({ ...localSettings, season: value }); // Rest of the code }
We can define the setLocalAttributes() like this:
// LeagueSettings.js function setLocalAttributes(value) { let newSettings = Object.assign(localSettings, value); localSettings = { ...newSettings }; setAttributes({ settings: localSettings }); }
So, we’re using Object.assign() to merge the two objects. Then we can clone the newSettings object back to localSettings because we also need to account for each settings attribute when there a new selection is made and a change occurs.
Finally, we can use the setAttributes() as we do normally to set the final object. You can confirm if the above attributes are changing by updating the selections in the UI.
Another way to confirm is to do a console.log() in DevTools to find the attributes.
Look closer at that screenshot. The values are stored in attributes.settings. We are able to see it happen live because React re-renders every time we make a change in the settings, thanks to the useState() hook.
Displaying the values in the blocks settings UI
It isn’t very useful to store the setting values in the control options themselves since each one is dependent on the other setting value (e.g. rankings by league depends on which season is selected). But it is very useful in situations where the settings values are static and where settings are independent of each other.
Without making the current settings complicated, we can create another section inside the settings panel that shows the current attributes. You can choose your way to display the settings values but I am going to import a Tip component from the @wordpress/components package:
// LeagueSettings.js import { Tip } from "@wordpress/components";
While I’m here, I am going to do a conditional check for the values before displaying them inside the Tip component:
Here’s how that winds up working in the block editor:
API data is more powerful when live data can be shown without having to manually update them each and every time. We will look into that in the next installment of this series.
So far, we’ve covered how to work with data from an external API in a custom WordPress block. We walked through the process of fetching that data for use on the front end of a WordPress site, and how to render it directly in the WordPress Block Editor when placing the block in content. This time, we’re going to bridge those two articles by hooking into the block editor’s control panel to create a settings UI for the block we made.
You know the control panel I’m referring to, right? It’s that panel on the right that contains post and block settings in the block editor.
See that red highlighted area? That’s the control panel. A Paragraph block is currently selected and the settings for it are displayed in the panel. We can change styles, color, typography… a number of things!
Well, that’s exactly what we’re doing this time around. We’re going to create the controls for the settings of the Football Rankings block we worked on in the last two articles. Last time, we made a button in our block that fetches the external data for the football rankings. We already knew the URL and endpoints we needed. But what if we want to fetch ranking for a different country? Or maybe a different league? How about data from a different season?
We need form controls to do that. We could make use of interactive React components — like React-Select — to browse through the various API options that are available to parse that data. But there’s no need for that since WordPress ships with a bunch of core components that we hook right into!
Before we hook into anything, it’s a good idea to map out what it is we need in the first place. I’ve mapped out the structure of the RapidAPI data we’re fetching so we know what’s available to us:
Seasons and countries are two top-level endpoints that map to a leagues endpoint. From there, we have the rest of the data we’re already using to populate the rankings table. So, what we want to do is create settings in the WordPress Block Editor that filter the data by Season, Country, and League, then pass that filtered data into the rankings table. That gives us the ability to drop the block in any WordPress page or post and display variations of the data in the block.
In order to get the standings, we need to first get the leagues. And in order to get the leagues, we first need to get the countries and/or the seasons. You can view the various endpoints in the RapidAPI dashboard.
There are different combinations of data that we can use to populate the rankings, and you might have a preference for which data you want. For the sake of this article, we are going to create the following options in the block settings panel:
Choose Country
Choose League
Choose Season
Then we’ll have a button to submit those selections and fetch the relevant data and pass them into the rankings table.
Load and store a list of countries
We can’t select which country we want data for if we don’t have a list of countries to choose from. So, our first task is to grab a list of countries from RapidAPI.
The ideal thing is to fetch the list of countries when the block is actually used in the page or post content. There’s no need to fetch anything if the block isn’t in use. The approach is very similar to what we did in the first article, the difference being that we are using a different API endpoint and different attributes to store the list of returned countries. There are other WordPress ways to fetch data, like api-fetch, but that‘s outside the scope of what we’re doing here.
We can either include the country list manually after copying it from the API data, or we could use a separate API or library to populate the countries. But the API we’re using already has a list of countries, so I would just use one of its endpoints. Let’s make sure the initial country list loads when the block is inserted into the page or post content in the block editor:
We have a state variable to store the list of countries. Next, we are going to import a component from the @wordpress/block-editor package called InspectorControls which is where all of the components we need to create our settings controls are located.
import { InspectorControls } from "@wordpress/block-editor";
The package’s GitHub repo does a good job explaining InspectorControls. In our example, we can use it to control the API data settings like Country, League, and Season. Here’s a preview so that you get an idea of the UI we’re making:
Here, I am making sure that we are using conditional rendering so that the function only loads the component after the list of countries is loaded. If you’re wondering about that LeagueSettings component, it is a custom component I created in a separate components subfolder in the block so we can have a cleaner and more organized Edit function instead of hundreds of lines of country data to deal with in a single file.
We can import it into the edit.js file like this:
import { LeagueSettings } from "./components/LeagueSettings";
Next, we’re passing the required props to the LeagueSettings component from the parent Edit component so that we can access the state variables and attributes from the LeagueSettings child component. We can also do that with other methods like the Context API to avoid prop drilling, but what we have right now is perfectly suitable for what we’re doing.
The other parts of the Edit function can also be converted into components. For example, the league standings code can be put inside a separate component — like maybe LeagueTable.js — and then imported just like we imported LeagueSettings into the Edit function.
Inside the LeagueSettings.js file
LeagueSettings is just like another React component from which we can destructure the props from the parent component. I am going to use three state variables and an additional leagueID state because we are going to extract the ID from the league object:
There are other panel tags and attributes — it’s just my personal preference to use these ones. None of the others are required… but look at all the components we have available to make a settings panel! I like the simplicity of the PanelBody for our use case. It expands and collapses to reveal the dropdown settings for the block and that’s it.
Speaking of which, we have a choice to make for those selections. We could use the SelectControl component or a ComboBoxControl, which the docs describe as “an enhanced version of a SelectControl, with the addition of being able to search for options using a search input.” That’s nice for us because the list of countries could get pretty long and users will be able to either do a search query or select from a list.
Here’s an example of how a ComboboxControl could work for our country list:
When a country is selected from the ComboboxControl, the country value changes and we filter the data accordingly:
function handleCountryChange(value) { // Set state of the country setCountry(value); // League code from RapidAPI const options = { method: "GET", headers: { "X-RapidAPI-Key": "Your RapidAPI key", "X-RapidAPI-Host": "api-football-v1.p.rapidapi.com", }, }; fetch(`https://api-football-v1.p.rapidapi.com/v3/leagues?country=$ {value}`, options) .then((response) => response.json()) .then((response) => { return response.response; }) .then((leagueOptions) => { // Set state of the league variable setLeague(leagueOptions); // Convert it as we did for Country options setupLeagueSelect = leagueOptions.map((league) => { return { label: league.league.name, value: league.league.name, }; }); setFilteredLeagueOptions(setupLeagueSelect); }) .catch((err) => console.error(err)); }
Note that I am using another three state variables to handle changes when the country selection changes:
I will show the code that I used for the other settings but all it does is take normal cases into account while defining errors for special cases. For example, there will be errors in some countries and leagues because:
there are no standings for some leagues, and
some leagues have standings but they are not in a single table.
This isn’t a JavaScript or React tutorial, so I will let you handle the special cases for the API that you plan to use:
In the last article, we made a button in the block editor that fetches fresh data from the API. There’s no more need for it now that we have settings. Well, we do need it — just not where it currently is. Instead of having it directly in the block that’s rendered in the block editor, we’re going to move it to our PanelBody component to submit the settings selections.
So, back in LeagueSettings.js:
// When countriesList is loaded, show the country combo box { countriesList && ( <ComboboxControl label="Choose country" value={country} options={filteredCountryOptions} onChange={(value) => handleCountryChange(value)} onInputChange={(inputValue) => { setFilteredCountryOptions( setupCountrySelect.filter((option) => option.label .toLowerCase() .startsWith(inputValue.toLowerCase()) ) ); }} /> )} // When filteredLeagueOptions is set through handleCountryChange, show league combobox { filteredLeagueOptions && ( <ComboboxControl label="Choose league" value={league} options={filteredLeagueOptions} onChange={(value) => handleLeagueChange(value)} onInputChange={(inputValue) => { setFilteredLeagueOptions( setupLeagueSelect.filter((option) => option.label .toLowerCase() .startsWith(inputValue.toLowerCase()) ) ); }} /> )} // When filteredSeasonOptions is set through handleLeagueChange, show season combobox { filteredSeasonOptions && ( <> <ComboboxControl label="Choose season" value={season} options={filteredSeasonOptions} onChange={(value) => handleSeasonChange(value)} onInputChange={ (inputValue) => { setFilteredSeasonOptions( setupSeasonSelect.filter((option) => option.label .toLowerCase() .startsWith(inputValue.toLowerCase() ) ); } } /> // When season is set through handleSeasonChange, show the "Fetch data" button { season && ( <button className="fetch-data" onClick={() => getData()}>Fetch data</button> ) } </> </> )}
Here’s the result!
We’re in a very good place with our block. We can render it in the block editor and the front end of the site. We can fetch data from an external API based on a selection of settings we created that filters the data. It’s pretty darn functional!
But there’s another thing we have to tackle. Right now, when we save the page or post that contains the block, the settings we selected for the block reset. In other words, those selections are not saved anywhere. There’s a little more work to make those selections persistent. That’s where we plan to go in the next article, so stay tuned.
In this article we will be diving into the world of scrollbars. I know, it doesn’t sound too glamorous, but trust me, a well-designed page goes hand-in-hand with a matching scrollbar. The old-fashioned chrome scrollbar just doesn’t fit in as much.
We will be looking into the nitty gritty details of a scrollbar and then look at some cool examples.
Components of a scrollbar
This is more of a refresher, really. There are a bunch of posts right here on CSS-Tricks that go into deep detail when it comes to custom scrollbar styling in CSS.
To style a scroll bar you need to be familiar with the anatomy of a scrollbar. Have a look at this illustration:
The two main components to keep in mind here are:
The track is the background beneath the bar.
The thumb is the part that the user clicks and drags around.
We can change the properties of the complete scrollbar using the vendor-prefixed::-webkit-scrollbar selector. We can give the scroll bar a fixed width, background color, rounded corners… lots of things! If we’re customizing the main scrollbar of a page, then we can use ::-webkit-scrollbar directly on the HTML element:
html::-webkit-scrollbar { /* Style away! */ }
If we’re customizing a scroll box that’s the result of overflow: scroll, then we can use ::-webkit-scrollbar on that element instead:
.element::-webkit-scrollbar { /* Style away! */ }
Here’s a quick example that styles the HTML element’s scrollbar so that it is wide with a red background:
What if we only want to change the scrollbar’s thumb or track? You guessed it — we have special prefixed pseudo-elements for those two: ::-webkit-scrollbar-thumb and ::-webkit-scrollbar-track, respectively. Here’s an idea of what’s possible when we put all of these things together:
Enough brushing up! I want to show you three degrees of custom scrollbar styling, then open up a big ol’ showcase of examples pulled from across the web for inspiration.
Simple and classy scrollbars
A custom scrollbar can still be minimal. I put together a group of examples that subtly change the appearance, whether with a slight color change to the thumb or track, or some light styling to the background.
As you can see, we don’t have to go nuts when it comes to scrollbar styling. Sometimes a subtle change is all it takes to enhance the overall user experience with a scrollbar that matches the overall theme.
But let’s admit it: it’s fun to go a little overboard and exercise some creativity. Here are some weird and unique scrollbars that might be “too much” in some cases, but they sure are eye-catching.
After Part 1 and Part 2, I am back with a third article to explore more fancy shapes. Like the previous articles, we are going to combine CSS Grid with clipping and masking to create fancy layouts for image galleries.
It’s not mandatory but highly recommended to cover as many tricks as possible. You can also read them in any order, but following along in chronological is a good idea to see how we arrived here.
Enough talking, let’s jump straight to our first example.
The Die Cut Photo Gallery
Before digging into the CSS, let’s check the markup:
Nothing but a few <img> tags in a div wrapper, right? Remember, the main challenge for this series is to work with the smallest amount of HTML possible. All the examples we’ve seen throughout this series use the exact same HTML markup. No extra divs, wrappers, and whatnot. All that we need are images contained in a wrapper element.
Basically, this is a square grid with three equal columns. From there, all that’s happening is the second and third images are explicitly placed on the grid, allowing the first and last images to lay out automatically around them.
This automatic behavior is a powerful feature of CSS Grid called “auto-placement”. Same thing with the number of rows — none of them are explicitly defined. The browser “implicitly” creates them based on the placement of the items. I have a very detailed article that explores both concepts.
You might be wondering what’s going on with those grid and grid-area property values. They look strange and are tough to grok! That’s because I chose the CSS grid shorthand property, which is super useful but accepts an unseemly number of values from its constituent properties. You can see all of them in the Almanac.
Same deal for the other grid-area declaration. When we put it all together, here’s what we get:
Yes, the second and third images are overlapped in the middle. That’s no mistake! I purposely spanned them on top of one another so that I can apply a clip-path to cut a portion from each one and get the final result:
How do we do that? We can cut the bottom-left corner of the second image (img:nth-child(2)) with the CSS clip-path property:
I know, I know. That’s a lot of numbers and whatnot. I do have an article that details the technique.
That’s it, we have our first grid of images! I added a grayscale filter on the <img> selector to get that neat little hover effect.
The Split Image Reveal
Let’s try something different. We can take what we learned about clipping the corner of an image and combine it with a nice effect to reveal the full image on hover.
The grid configuration for this one is less intense than the last one, as all we need are two overlapping images:
Two images that are the same size are stacked on top of each other (thanks to grid-area: 1 / 1).
The hover effect relies on animating clip-path. We will dissect the code of the first image to see how it works, then plug the same thing into the second image with updated values. Notice, though that we have three different states:
When no images are hovered, half of each image is revealed.
When we hover over the first image, it is more fully revealed but retains a small corner clip.
When we hover over the second image, the first one has only a small triangle visible.
In each case, we have a triangular shape. That means we need a three-point polygon for the clip-path value.
What? The second state isn’t a triangle, but more of a square with a cut corner.
You are right, but if we look closely we can see a “hidden” triangle. Let’s add a box-shadow to the images.
A ha! Did you notice it?
What sort of magic is this? It’s a little known fact that clip-path accepts values outside the 0%-100% range, which allows us to create “overflowing” shapes. (Yes, I just coined this. You’re welcome.) This way, we only have to work with three points instead of the five it would take to make the same shape from the visible parts. Optimized CSS for the win!
This is the code after we plug in the polygon values into the clip-path property:
Notice the --_p variable. I’m using that to optimize the code a bit as we add the hover transition. Instead of updating the whole clip-path we only update this variable to get the movement. Here is a video to see how the points should move between each state:
We can take slap a transition on the <img> selector, then update the --_p variable on the states to get the final effect:
If we don’t consider the gap (defined as --g in the code) between the images, then the three values of --_p are 0%, 50%, and -50%. Each one defines one of the states we explained previously.
The Pie Image Reveal
Let’s increase the difficulty level from that last one and try to do the same trick but with four images instead of two.
Cool, right? Each image is a quarter of a circle and, on hover, we have an animation that transforms an image into a full circle that covers the remaining images. The effect may look impossible because there is no way to rotate points and transform them to fill the circle. In reality, though, we are not rotating any points at all. It’s an illusion!
For this example, I will only focus on the clip-path animation since the configuration of the grid is the same as the previous example: four equally-sized images stacked on top of each other.
And a video worth a boring and long explanation:
The clip-path is formed by seven points, where three of them are in a fixed position and the others move as shown in the video. The effect looks less cool when it’s running slowly but we can see how the clip-path morphs between shapes.
The effect is a little better if we add border-radius and we make it faster:
And by making it even faster like in the original example, we get the perfect illusion of one quarter of a circle morphing into a full circle. Here’s the polygon value for our clip-path on the first image in the sequence:
As usual, I am using a variable to optimize the code. The variable will switch between 0 and 1 to update the polygon.
The same goes for the others image but with a different clip-path configuration. I know that the values may look hard to decipher but you can always use online tools like Clippy to visualize the values.
The Mosaic of Images
You know mosaics, right? It’s an art style that creates decorative designs out of smaller individual pieces, like colored stones. But it can also be a composite image made up of other smaller images.
And, you guessed it: we can totally do that sort of thing in CSS!
First, let’s imagine what things are like if clip-path were taken out of the mix and all we had were five overlapping images:
I am cheating a little in this video because I am inspecting the code to identify the area of each image, but this is what you need to do in your head. For each image, try to complete the missing part to see the full rectangle and, with this, we can identify the position and size of each one.
We need to find how many columns and rows we need for the grid:
We have two big images placed next to each other that each fill half the grid width and the full grid height. That means will probably need two columns (one for both images) and one row (for the full height of the grid).
We have the image in the middle that overlaps the two other images. That means we actually need four columns instead of two, though we still only need the one row.
The last two images each fill half the grid, just like the first two images. But they’re only half the height of the grid. We can use the existing columns we already have, but we’re going to need two rows instead of one to account for these images being half the grid height.
That leaves us with a tidy 4×2 grid.
I don’t want you to think that the way I sliced this up is the only way to do it. This is merely how I’ve made sense of it. I am sure there are other configurations possible to get the same layout!
Let’s take that information and define our grid, then place the images on it:
I think you get the idea of what’s happening here now that we’ve seen a few examples using the same approach. We define a grid and place images on it explicitly, using grid-area so the images overlap.
OK, but the aspect-ratio is different this time.
It is! If you get back to the reasoning we made, we have the first two images that are square next to each other having the same size. This means that the width of the grid needs to be equal to twice its height. Hence, aspect-ratio: 2.
Now it’s time for the clip-path values. We have four triangles and a rhombus.
We’re only showing the three unique shapes we’re making instead of the five total shapes.
Again, I’m using Clippy for all this math-y stuff. But, honestly, I can write many simple shapes by hand, having spent several years working closely with clip-path, and I know you can too with practice!
The Complex Mosaic of Images
Let’s increase the difficulty and try another mosaic, this time with less symmetry and more complex shapes.
Don’t worry, you will see that it’s the same concept as the one we just made! Again, let’s imagine each image is a rectangle, then go about defining the grid based on what we see.
We’ll start with two images:
They are both squares. The first image is equal to half the size of the second image. The first image takes up less than one half of the grid width, while the second image takes up more than half giving us a total of two columns with a different size (the first one is equal to half the second one). The first image is half the height, so let’s automatically assume we need two rows as well.
Let’s add another image to the layout
This one makes things a bit more complex! We need to draw some lines to identify how to update the grid configuration.
We will move from a 2×2 grid to four columns and three rows. Pretty asymmetric, right? Before we try to figure out that complete sizing, let’s see if the same layout holds up when we add the other images.
Looks like we still need more rows and columns for everything to fall into place. Based on the lines in that image, we’re going to have a total of five columns and four rows.
The logic is simple even though the layout is complex, right? We add the images one by one to find the right configuration that fits everything. Now we need to identify the size of each column and row.
If we say the smallest row/column is equal to one fraction of the grid (1fr) we will get:
grid-template-columns: 1fr 1fr 2fr 3fr 5fr;
…for the columns, and:
grid-template-rows: 3fr 1fr 2fr 2fr;
…for the rows. We can consolidate this using the grid shorthand property again:
grid: 3fr 1fr 2fr 2fr / 1fr 1fr 2fr 3fr 5fr;
You know the drill! Place the images on the grid and apply a clip-path on them:
We can stop here and our code is fine, but we will do a little more to optimize the clip-path values. Since we don’t have any gaps between our images, we can use the fact that our images overlap to slim things down. Here is a video to illustrate the idea:
As you can see, the image in the middle (the one with the camera) doesn’t need a clip-path. because the other images overlap it, giving us the shape without any additional work! And notice that we can use the same overflowing three-point clip-path concept we used earlier on the image in the bottom-left to keep the code smaller there as well.
In the end, we have a complex-looking grid of images with only four clip-path declarations — all of them are three-point polygons!
Wrapping up
Wow, right? I don’t know about you, but I never get bored of seeing what CSS can do these days. It wasn’t long ago that all of this would have taken verbose hackery and definitely some JavaScript.
Throughout this series, we explored many, many different types of image grids, from the basic stuff to the complex mosaics we made today. And we got a lot of hands-on experience working with CSS clipping — something that you will definitely be able to use on other projects!
But before we end this, I have some homework for you…
Here are two mosaics that I want you to make using what we covered here. One is on the “easier” side, and the other is a bit tricky. It would be really awesome to see your work in the comments, so link them up! I’m curious to see if your approach is different from how I’d go about it!
SVG is the best format for icons on a website, there is no doubt about that. It allows you to have sharp icons no matter the screen pixel density, you can change the styles of the SVG on hover and you can even animate the icons with CSS or JavaScript.
There are many ways to include an SVG on a page and each technique has its own advantages and disadvantages. For the last couple of years, I have been using a Sass function to import directly my icons in my CSS and avoid having to mess up my HTML markup.
I have a Sass list with all the source codes of my icons. Each icon is then encoded into a data URI with a Sass function and stored in a custom property on the root of the page.
TL;DR
What I have for you here is a Sass function that creates a SVG icon library directly in your CSS.
The SVG source code is compiled with the Sass function that encodes them in data URI and then stores the icons in CSS custom properties. You can then use any icon anywhere in your CSS like as if it was an external image.
This is an example pulled straight from the code of my personal site:
/* All the icons source codes */ $ svg-icons: ( burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0...' ); /* Sass function to encode the icons */ @function svg($ name) { @return url('data:image/svg+xml, #{$ encodedSVG} '); } /* Store each icon into a custom property */ :root { @each $ name, $ code in $ svg-icons { --svg-#{$ name}: #{svg($ name)}; } } /* Append a burger icon in my button */ .menu::after { content: var(--svg-burger); }
This technique has both pros and cons, so please take them into account before implementing this solution on your project:
Pros
There are no HTTP requests for the SVG files.
All of the icons are stored in one place.
If you need to update an icon, you don’t have to go over each HTML templates file.
The icons are cached along with your CSS.
You can manually edit the source code of the icons.
It does not pollute your HTML by adding extra markup.
You can still change the color or some aspect of the icon with CSS.
Cons
You cannot animate or update a specific part of the SVG with CSS.
The more icons you have, the heavier your CSS compiled file will be.
I mostly use this technique for icons rather than logos or illustrations. An encoded SVG is always going to be heavier than its original file, so I still load my complex SVG with an external file either with an <img> tag or in my CSS with url(path/to/file.svg).
Encoding SVG into data URI
Encoding your SVG as data URIs is not new. In fact Chris Coyier wrote a post about it over 10 years ago to explain how to use this technique and why you should (or should not) use it.
There are two ways to use an SVG in your CSS with data URI:
As an external image (using background-image,border-image,list-style-image,…)
As the content of a pseudo element (e.g. ::before or ::after)
Here is a basic example showing how you how to use those two methods:
The main issue with this particular implementation is that you have to convert the SVG manually every time you need a new icon and it is not really pleasant to have this long string of unreadable code in your CSS.
This is where Sass comes to the rescue!
Using a Sass function
By using Sass, we can make our life simpler by copying the source code of our SVG directly in our codebase, letting Sass encode them properly to avoid any browser error.
This solution is mostly inspired by an existing function developed by Threespot Media and available in their repository.
Here are the four steps of this technique:
Create a variable with all your SVG icons listed.
List all the characters that needs to be skipped for a data URI.
Implement a function to encode the SVGs to a data URI format.
Use your function in your code.
1. Icons list
/** * Add all the icons of your project in this Sass list */ $ svg-icons: ( burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>' );
2. List of escaped characters
/** * Characters to escape from SVGs * This list allows you to have inline CSS in your SVG code as well */ $ fs-escape-chars: ( ' ': '%20', '\'': '%22', '"': '%27', '#': '%23', '/': '%2F', ':': '%3A', '(': '%28', ')': '%29', '%': '%25', '<': '%3C', '>': '%3E', '\': '%5C', '^': '%5E', '{': '%7B', '|': '%7C', '}': '%7D', );
3. Encode function
/** * You can call this function by using `svg(nameOfTheSVG)` */ @function svg($ name) { // Check if icon exists @if not map-has-key($ svg-icons, $ name) { @error 'icon “#{$ name}” does not exists in $ svg-icons map'; @return false; } // Get icon data $ icon-map: map-get($ svg-icons, $ name); $ escaped-string: ''; $ unquote-icon: unquote($ icon-map); // Loop through each character in string @for $ i from 1 through str-length($ unquote-icon) { $ char: str-slice($ unquote-icon, $ i, $ i); // Check if character is in symbol map $ char-lookup: map-get($ fs-escape-chars, $ char); // If it is, use escaped version @if $ char-lookup != null { $ char: $ char-lookup; } // Append character to escaped string $ escaped-string: $ escaped-string + $ char; } // Return inline SVG data @return url('data:image/svg+xml, #{$ escaped-string} '); }
The now-implemented Sass svg() function works great. But its biggest flaw is that an icon that is needed in multiple places in your code will be duplicated and could increase your compiled CSS file weight by a lot!
To avoid this, we can store all our icons into CSS variables and use a reference to the variable instead of outputting the encoded URI every time.
We will keep the same code we had before, but this time we will first output all the icons from the Sass list into the root of our webpage:
/** * Convert all icons into custom properties * They will be available to any HTML tag since they are attached to the :root */ :root { @each $ name, $ code in $ svg-icons { --svg-#{$ name}: #{svg($ name)}; } }
Now, instead of calling the svg() function every time we need an icon, we have to use the variable that was created with the --svg prefix.
This technique does not provide any optimization on the source code of the SVG you are using. Make sure that you don’t leave unnecessary code; otherwise they will be encoded as well and will increase your CSS file size.
You can check this great list of tools and information on how to optimize properly your SVG. My favorite tool is Jake Archibald’s SVGOMG — simply drag your file in there and copy the outputted code.
Bonus: Updating the icon on hover
With this technique, we cannot select with CSS specific parts of the SVG. For example, there is no way to change the fill color of the icon when the user hovers the button. But there are a few tricks we can use with CSS to still be able to modify the look of our icon.
For example, if you have a black icon and you want to have it white on hover, you can use the invert() CSS filter. We can also play with the hue-rotate() filter.
That’s it!
I hope you find this little helper function handy in your own projects. Let me know what you think of the approach — I’d be interested to know how you’d make this better or tackle it differently!
If a utility class only does one thing, chances are you don’t want it to be overridden by any styles coming from elsewhere. One approach is to use !important to be 100% certain the style will be applied, regardless of specificity conflicts.
The Tailwind config file has an !important option that will automatically add !important to every utility class. There’s nothing wrong with using !important this way, but nowadays there are better ways to handle specificity. Using CSS Cascade Layers we can avoid the heavy-handed approach of using !important.
Cascade layers allow us to group styles into “layers”. The precedence of a layer always beats the specificity of a selector. Specificity only matters inside each layer. Establishing a sensible layer order helps avoid styling conflicts and specificity wars. That’s what makes CSS Cascade Layers a great tool for managing custom styles alongside styles from third-party frameworks, like Tailwind.
A Tailwind source .css file usually starts something like this:
Directives are custom Tailwind-specific at-rules you can use in your CSS that offer special functionality for Tailwind CSS projects. Use the @tailwind directive to insert Tailwind’s base, components, utilities and variants styles into your CSS.
In the output CSS file that gets built, Tailwind’s CSS reset — known as Preflight — is included first as part of the base styles. The rest of base consists of CSS variables needed for Tailwind to work. components is a place for you to add your own custom classes. Any utility classes you’ve used in your markup will appear next. Variants are styles for things like hover and focus states and responsive styles, which will appear last in the generated CSS file.
The Tailwind @layer directive
Confusingly, Tailwind has its own @layer syntax. This article is about the CSS standard, but let’s take a quick look at the Tailwind version (which gets compiled away and doesn’t end up in the output CSS). The Tailwind @layer directive is a way to inject your own extra styles into a specified part of the output CSS file.
For example, to append your own styles to the base styles, you would do the following:
@layer base { h1 { font-size: 30px; } }
The components layer is empty by default — it’s just a place to put your own classes. If you were doing things the Tailwind way, you’d probably use @apply (although the creator of Tailwind recently advised against it), but you can also write classes the regular way:
Unlike the Tailwind directive, these don’t get compiled away. They’re understood by the browser. In fact, DevTools in Edge, Chrome, Safari, and Firefox will even show you any layers you’ve defined.
You can have as many layers as you want — and name them whatever you want — but in this example, all my custom styles are in a single layer (my-custom-styles). The first line establishes the layer order:
This needs to be provided upfront. Be sure to include this line before any other code that uses @layer. The first layer in the list will be the least powerful, and the last layer in the list will be the most powerful. That means tailwind-base is the least powerful layer and any code in it will be overridden by all the subsequent layers. That also means tailwind-utilities will always trump any other styles — regardless of source order or specificity. (Utilities and variants could go in separate layers, but the maintainers of Tailwind will ensure variants always trump utilities, so long as you include the variants below the utilities directive.)
Anything that isn’t in a layer will override anything that is in a layer (with the one exception being styles that use !important). So, you could also opt to leave utilities and variants outside of any layer:
What did this actually buy us? There are plenty of times when advanced CSS selectors come in pretty handy. Let’s create a version of :focus-within that only responds to keyboard focus rather than mouse clicks using the :has selector (which lands in Chrome 105). This will style a parent element when any of its children receive focus. Tailwind 3.1 introduced custom variants — e.g. <div class="[&:has(:focus-visible)]:outline-red-600"> — but sometimes it’s easier to just write CSS:
Let’s say in just one instance we want to override the outline-color from blue to something else. Let’s say the element we’re working with has both the Tailwind class .outline-red-600 and our own .radio-container:has(:focus-visible) class:
Ordinarily, the higher specificity of .radio-container:has(:focus-visible) would mean the Tailwind class has no effect — even if it’s lower in the source order. But, unlike the Tailwind @layer directive that relies on source order, the CSS standard @layer overrules specificity.
As a result, we can use complex selectors in our own custom styles but still override them with Tailwind’s utility classes when we need to — without having to resort to heavy-handed !important usage to get what we want.
Alright, so the last time we checked in, we were using CSS Grid and combining them with CSS clip-path and mask techniques to create grids with fancy shapes.
Here’s just one of the fantastic grids we made together:
Ready for the second round? We are still working with CSS Grid, clip-path, and mask, but by the end of this article, we’ll end up with different ways to arrange images on the grid, including some rad hover effects that make for an authentic, interactive experience to view pictures.
And guess what? We’re using the same markup that we used last time. Here’s that again:
<div class="gallery"> <img src="..." alt="..."> <img src="..." alt="..."> <img src="..." alt="..."> <img src="..." alt="..."> <!-- as many times as we want --> </div>
Like the previous article, we only need a container with images inside. Nothing more!
Nested Image Grid
Last time, our grids were, well, typical image grids. Other than the neat shapes we masked them with, they were pretty standard symmetrical grids as far as how we positioned the images inside.
Let’s try nesting an image in the center of the grid:
We start by setting a 2✕2 grid for four images:
.gallery { --s: 200px; /* controls the image size */ --g: 10px; /* controls the gap between images */ display: grid; gap: var(--g); grid-template-columns: repeat(2, auto); } .gallery > img { width: var(--s); aspect-ratio: 1; object-fit: cover; }
Nothing complex yet. The next step is to cut the corner of our images to create the space for the nested image. I already have a detailed article on how to cut corners using clip-path and mask. You can also use my online generator to get the CSS for masking corners.
What we need here is to cut out the corners at an angle equal to 90deg. We can use the same conic-gradient technique from that article to do that:
We could use the clip-path method for cutting corners from that same article, but masking with gradients is more suitable here because we have the same configuration for all the images — all we need is a rotation (defined with the variable --_a) get the effect, so we’re masking from the inside instead of the outside edges.
Now we can place the nested image inside the masked space. First, let’s make sure we have a fifth image element in the HTML:
The inset property allows us to place the image at the center using a single declaration. We know the size of the image (defined with the variable --s), and we know that the container’s size equals 100%. We do some math, and the distance from each edge should be equal to (100% - var(--s))/2.
You might be wondering why we’re using clip-path at all here. We’re using it with the nested image to have a consistent gap. If we were to remove it, you would notice that we don’t have the same gap between all the images. This way, we’re cutting a little bit from the fifth image to get the proper spacing around it.
Now, many of you might also be wondering: why all the complex stuff when we can place the last image on the top and add a border to it? That would hide the images underneath the nested image without a mask, right?
That’s true, and we will get the following:
No mask, no clip-path. Yes, the code is easy to understand, but there is a little drawback: the border color needs to be the same as the main background to make the illusion perfect. This little drawback is enough for me to make the code more complex in exchange for real transparency independent of the background. I am not saying a border approach is bad or wrong. I would recommend it in most cases where the background is known. But we are here to explore new stuff and, most important, build components that don’t depend on their environment.
Let’s try another shape this time:
This time, we made the nested image a circle instead of a square. That’s an easy task with border-radius But we need to use a circular cut-out for the other images. This time, though, we will rely on a radial-gradient() instead of a conic-gradient() to get that nice rounded look.
All the images use the same configuration as the previous example, but we update the center point each time.
The above figure illustrates the center point for each circle. Still, in the actual code, you will notice that I am also accounting for the gap to ensure all the points are at the same position (the center of the grid) to get a continuous circle if we combine them.
Now that we have our layout let’s talk about the hover effect. In case you didn’t notice, a cool hover effect increases the size of the nested image and adjusts everything else accordingly. Increasing the size is a relatively easy task, but updating the gradient is more complicated since, by default, gradients cannot be animated. To overcome this, I will use a font-size hack to be able to animate the radial gradient.
If you check the code of the gradient, you can see that I am adding 1em:
It’s known that em units are relative to the parent element’s font-size, so changing the font-size of the .gallery will also change the computed em value — this is the trick we are using. We are animating the font-size from a value of 0 to a given value and, as a result, the gradient is animated, making the cut-out part larger, following the size of the nested image that is getting bigger.
Here is the code that highlights the parts involved in the hover effect:
.gallery { --s: 200px; /* controls the image size */ --g: 10px; /* controls the gaps between images */ font-size: 0; /* initially we have 1em = 0 */ transition: .5s; } /* we increase the cut-out by 1em */ .gallery > img { mask: radial-gradient(farthest-side at var(--_a), #0000 calc(50% + var(--g)/2 + 1em), #000 calc(51% + var(--g)/2 + 1em)); } /* we increase the size by 2em */ .gallery > img:nth-child(5) { width: calc(var(--s) + 2em); } /* on hover 1em = S/5 */ .gallery:hover { font-size: calc(var(--s) / 5); }
The font-size trick is helpful if we want to animate gradients or other properties that cannot be animated. Custom properties defined with @property can solve such a problem, but support for it is still lacking at the time of writing.
This time we clipped the nested image into the shape of a rhombus. I’ll let you dissect the code as an exercise to figure out how we got here. You will notice that the structure is the same as in our examples. The only differences are how we’re using the gradient to create the shape. Dig in and learn!
Circular Image Grid
We can combine what we’ve learned here and in previous articles to make an even more exciting image grid. This time, let’s make all the images in our grid circular and, on hover, expand an image to reveal the entire thing as it covers the rest of the photos.
The HTML and CSS structure of the grid is nothing new from before, so let’s skip that part and focus instead on the circular shape and hover effect we want.
We are going to use clip-path and its circle() function to — you guessed it! — cut a circle out of the images.
That figure illustrates the clip-path used for the first image. The left side shows the image’s initial state, while the right shows the hovered state. You can use this online tool to play and visualize the clip-path values.
For the other images, we can update the center of the circle (70% 70%) to get the following code:
.gallery > img:hover { --_c: 50%; /* same as "50% at 50% 50%" */ } .gallery > img:nth-child(1) { clip-path: circle(var(--_c, 55% at 70% 70%)); } .gallery > img:nth-child(2) { clip-path: circle(var(--_c, 55% at 30% 70%)); } .gallery > img:nth-child(3) { clip-path: circle(var(--_c, 55% at 70% 30%)); } .gallery > img:nth-child(4) { clip-path: circle(var(--_c, 55% at 30% 30%)); }
Note how we are defining the clip-path values as a fallback inside var(). This way allows us to more easily update the value on hover by setting the value of the --_c variable. When using circle(), the default position of the center point is 50% 50%, so we get to omit that for more concise code. That’s why you see that we are only setting 50% instead of 50% at 50% 50%.
Then we increase the size of our image on hover to the overall size of the grid so we can cover the other images. We also ensure the z-index has a higher value on the hovered image, so it is the top one in our stacking context.
.gallery { --s: 200px; /* controls the image size */ --g: 8px; /* controls the gap between images */ display: grid; grid: auto-flow var(--s) / repeat(2, var(--s)); gap: var(--g); } .gallery > img { width: 100%; aspect-ratio: 1; cursor: pointer; z-index: 0; transition: .25s, z-index 0s .25s; } .gallery > img:hover { --_c: 50%; /* change the center point on hover */ width: calc(200% + var(--g)); z-index: 1; transition: .4s, z-index 0s; } .gallery > img:nth-child(1){ clip-path: circle(var(--_c, 55% at 70% 70%)); place-self: start; } .gallery > img:nth-child(2){ clip-path: circle(var(--_c, 55% at 30% 70%)); place-self: start end; } .gallery > img:nth-child(3){ clip-path: circle(var(--_c, 55% at 70% 30%)); place-self: end start; } .gallery > img:nth-child(4){ clip-path: circle(var(--_c, 55% at 30% 30%)); place-self: end; }
What’s going on with the place-self property? Why do we need it and why does each image have a specific value?
Do you remember the issue we had in the previous article when creating the grid of puzzle pieces? We increased the size of the images to create an overflow, but the overflow of some images was incorrect. We fixed them using the place-self property.
Same issue here. We are increasing the size of the images so each one overflows its grid cells. But if we do nothing, all of them will overflow on the right and bottom sides of the grid. What we need is:
the first image to overflow the bottom-right edge (the default behavior),
the second image to overflow the bottom-left edge,
the third image to overflow the top-right edge, and
the fourth image to overflow the top-left edge.
To get that, we need to place each image correctly using the place-self property.
In case you are not familiar with place-self, it’s the shorthand for justify-self and align-self to place the element horizontally and vertically. When it takes one value, both alignments use that same value.
Expanding Image Panels
In a previous article, I created a cool zoom effect that applies to a grid of images where we can control everything: number of rows, number of columns, sizes, scale factor, etc.
A particular case was the classic expanding panels, where we only have one row and a full-width container.
We will take this example and combine it with shapes!
Before we continue, I highly recommend reading my other article to understand how the tricks we’re about to cover work. Check that out, and we’ll continue here to focus on creating the panel shapes.
First, let’s start by simplifying the code and removing some variables
We only need one row and the number of columns should adjust based on the number of images. That means we no longer need variables for the number of rows (--n) and columns (--m ) but we need to use grid-auto-flow: column, allowing the grid to auto-generate columns as we add new images. We will consider a fixed height for our container; by default, it will be full-width.
Once again, each image is contained in its grid cell, so there’s more space between the images than we’d like:
We need to increase the width of the images to create an overlap. We replace min-width:100% with min-width: calc(100% + var(--s)), where --s is a new variable that controls the shape.
Now we need to fix the first and last images, so they sort of bleed off the page without gaps. In other words, we can remove the slant from the left side of the first image and the slant from the right side of the last image. We need a new clip-path specifically for those two images.
We also need to rectify the overflow. By default, all the images will overflow on both sides, but for the first one, we need an overflow on the right side while we need a left overflow for the last image.
The final result is a nice expanding panel of slanted images!
We can add as many images as you want, and the grid will adjust automatically. Plus, we only need to control one value to control the shape!
We could have made this same layout with flexbox since we are dealing with a single row of elements. Here is my implementation.
Sure, slanted images are cool, but what about a zig-zag pattern? I already teased this one at the end of the last article.
All I’m doing here is replacing clip-path with mask… and guess what? I already have a detailed article on creating that zig-zag shape — not to mention an online generator to get the code. See how all everything comes together?
The trickiest part here is to make sure the zig-zags are perfectly aligned, and for this, we need to add an offset for every :nth-child(odd) image element.
Note the use of the --_p variable, which will fall back to 0% but will be equal to --_s for the odd images.
Here is a demo that illustrates the issue. Hover to see how the offset — defined by --_p — is fixing the alignment.
Also, notice how we use a different mask for the first and last image as we did in the previous example. We only need a zig-zag on the right side of the first image and the left side of the last image.
And why not rounded sides? Let’s do it!
I know that the code may look scary and tough to understand, but all that’s going on is a combination of different tricks we’ve covered in this and other articles I’ve already shared. In this case, I use the same code structure as the zig-zag and the slanted shapes. Compare it with those examples, and you will find no difference! Those are the same tricks in my previous article about the zoom effect. Then, I am using my other writing and my online generator to get the code for the mask that creates those rounded shapes.
If you recall what we did for the zig-zag, we had used the same mask for all the images but then had to add an offset to the odd images to create a perfect overlap. In this case, we need a different mask for the odd-numbered images.
The only effort I did here is update the second mask to include the gap variable (--g) to create that space between the images.
The final touch is to fix the first and last image. Like all the previous examples, the first image needs a straight left edge while the last one needs a straight right edge.
For the first image, we always know the mask it needs to have, which is the following:
That’s all! Three different layouts but the same CSS tricks each time:
the code structure to create the zoom effect
a mask or clip-path to create the shapes
a separate configuration for the odd elements in some cases to make sure we have a perfect overlap
a specific configuration for the first and last image to keep the shape on only one side.
And here is a big demo with all of them together. All you need is to add a class to activate the layout you want to see.
And here is the one with the Flexbox implementation
Wrapping up
Oof, we are done! I know there are many CSS tricks and examples between this article and the last one, not to mention all of the other tricks I’ve referenced here from other articles I’ve written. It took me time to put everything together, and you don’t have to understand everything at once. One reading will give you a good overview of all the layouts, but you may need to read the article more than once and focus on each example to grasp all the tricks.
Did you notice that we didn’t touch the HTML at all other than perhaps the number of images in the markup? All the layouts we made share the same HTML code, which is nothing but a list of images.
Before I end, I will leave you with one last example. It’s a “versus” between two anime characters with a cool hover effect.
What about you? Can you create something based on what you have learned? It doesn’t need to be complex — imagine something cool or funny like I did with that anime matchup. It can be a good exercise for you, and we may end with an excellent collection in the comment section.
Like, what if the images aren’t perfectly square but instead are shaped like hexagons or rhombuses? Spoiler alert: we can do it. In fact, we’re going to combine CSS Grid techniques we’ve looked at and drop in some CSS clip-path and mask magic to create fancy grids of images for just about any shape you can imagine!
Let’s start with some markup
Most of the layouts we are going to look at may look easy to achieve at first glance, but the challenging part is to achieve them with the same HTML markup. We can use a lot of wrappers, divs, and whatnot, but the goal of this post is to use the same and smallest amount of HTML code and still get all the different grids we want. After all, what’s CSS but a way to separate styling and markup? Our styling should not depend on the markup, and vice versa.
This said, let’s start with this:
<div class="gallery"> <img src="..." alt="..."> <img src="..." alt="..."> <img src="..." alt="..."> <img src="..." alt="..."> <!-- as many times as we want --> </div>
A container with images is all that we need here. Nothing more!
CSS Grid of Hexagons
This is also sometimes referred to as a “honeycomb” grid.
There are already plenty of other blog posts out there that show how to make this. Heck, I wrote one here on CSS-Tricks! That article is still good and goes way deep on making a responsive layout. But for this specific case, we are going to rely on a much simpler CSS approach.
First, let’s use clip-path on the images to create the hexagon shape and we place all of them in the same grid area so they overlap.
Nothing fancy yet. All the images are hexagons and above each other. So it looks like all we have is a single hexagon-shaped image element, but there are really seven.
The next step is to apply a translation to the images to correctly place them on the grid.
Notice that we still want one of the images to remain in the center. The rest are placed around it using CSS translate and good ol’ fashioned geometry. Here’s are the mock formulas I came up with for each image in the grid:
Each image is translated by the --_x and --_y variables that are based on those formulas. Only the second image (nth-child(2)) is undefined in any selector because it’s the one in the center. It can be any image if you decide to use a different order. Here’s the order I’m using:
With only a few lines of code, we get a cool grid of images. To this, I added a little hover effect to the images to make things fancier.
Guess what? We can get another hexagon grid by simply updating a few values.
If you check the code and compare it with the previous one you will notice that I have simply swapped the values inside clip-path and I switched between --x and --y. That’s all!
CSS Grid of Rhombuses
Rhombus is such a fancy word for a square that’s rotated 45 degrees.
Same HTML, remember? We first start by defining a 2×2 grid of images in CSS:
The first thing that might catch your eye is the grid property. It’s pretty uncommonly used but is super helpful in that it’s a shorthand that lets you define a complete grid in one declaration. It’s not the most intuitive — and not to mention readable — property, but we are here to learn and discover new things, so let’s use it rather than writing out all of the individual grid properties.
grid: auto-flow var(--s) / repeat(2,var(--s)); /* is equivalent to this: */ grid-template-columns: repeat(2, var(--s)); grid-auto-rows: var(--s);
This defines two columns equal to the --s variable and sets the height of all the rows to --s as well. Since we have four images, we will automatically get a 2×2 grid.
After setting the grid, we rotate it and the images with CSS transforms and we get this:
Note how I rotate them both by 45deg, but in the opposite direction.
.gallery { /* etc. */ transform: rotate(45deg); } .gallery > img { /* etc. */ transform: rotate(-45deg); }
Rotating the images in the negative direction prevents them from getting rotated with the grid so they stay straight. Now, we apply a clip-path to clip a rhombus shape out of them.
We are almost done! We need to rectify the size of the image to make them fit together. Otherwise, they’re spaced far apart to the point where it doesn’t look like a grid of images.
The image is within the boundary of the green circle, which is the inscribed circle of the grid area where the image is placed. What we want is to make the image bigger to fit inside the red circle, which is the circumscribed circle of the grid area.
Don’t worry, I won’t introduce any more boring geometry. All you need to know is that the relationship between the radius of each circle is the square root of 2 (sqrt(2)). This is the value we need to increase the size of our images to fill the area. We will use 100%*sqrt(2) = 141% and be done!
Like the hexagon grid, we can make things fancier with that nice zooming hover effect:
CSS Grid of Triangular Shapes
You probably know by now that the big trick is figuring out the clip-path to get the shapes we want. For this grid, each element has its own clip-path value whereas the last two grids worked with a consistent shape. So, this time around, it’s like we’re working with a few different triangular shapes that come together to form a rectangular grid of images.
The three images at the topThe three images at the bottom
We place them inside a 3×2 grid with the following CSS:
The final touch is to make the width of the middle column equal 0 to get rid of the spaces between the images. The same sort of spacing problem we had with the rhombus grid, but with a different approach for the shapes we’re using:
grid-template-columns: auto 0 auto;
I had to fiddle with the clip-path values to make sure they would all appear to fit together nicely like a puzzle. The original images overlap when the middle column has zero width, but after slicing the images, the illusion is perfect:
CSS Pizza Pie Grid
Guess what? We can get another cool grid by simply adding border-radius and overflow to our grid or triangular shapes. 🎉
CSS Grid of Puzzle Pieces
This time we are going to play with the CSS mask property to make the images look like pieces of a puzzle.
If you haven’t used mask with CSS gradients, I highly recommend this other article I wrote on the topic because it’ll help with what comes next. Why gradients? Because that’s what we’re using to get the round notches in the puzzle piece shapes.
Setting up the grid should be a cinch by now, so let’s focus instead on the mask part.
As illustrated in the above demo, we need two gradients to create the final shape. One gradient creates a circle (the green part) and the other creates the right curve while filling in the top part.
--g: 6px; /* controls the gap */ --r: 42px; /* control the circular shapes */ background: radial-gradient(var(--r) at left 50% bottom var(--r), green 95%, #0000), radial-gradient(calc(var(--r) + var(--g)) at calc(100% + var(--g)) 50%, #0000 95%, red) top/100% calc(100% - var(--r)) no-repeat;
Two variables control the shape. The --g variable is nothing but the grid gap. We need to account for the gap to correctly place our circles so they overlap perfectly when the whole puzzle is assembled. The --r variable controls the size of circular parts of the puzzle shape.
Now we take the same CSS and update a few values in it to create the three other shapes:
We have the shapes, but not the overlapping edges we need to make them fit together. Each image is limited to the grid cell it’s in, so it makes sense why the shapes are sort of jumbled at the moment:
We need to create an overflow by increasing the height/width of the images. From the above figure, we have to increase the height of the first and fourth images while we increase the width of the second and third ones. You have probably already guessed that we need to increase them using the --r variable.
We created the overlap but, by default, our images either overlap on the right (if we increase the width) or the bottom (if we increase the height). But that’s not what we want for the second and fourth images. The fix is to use place-self: end on those two images and our full code becomes this:
Here is another example where I am using a conic gradient instead of a radial gradient. This gives us triangular puzzle pieces while keeping the same underlying HTML and CSS.
A last one! This time I am using clip-path and since it’s a property we can animate, we get a cool hover by simply updating the custom property that controls the shape.
Wrapping up
That’s all for this first part! By combining the things we’ve already learned about CSS Grid with some added clip-path and mask magic, we were able to make grid layouts featuring different kinds of shapes. And we used the same HTML markup each time! And the markup itself is nothing more than a container with a handful of image elements!
In the second part, we are going to explore more complex-looking grids with more fancy shapes and hover effects.
I’m planning to take the demo of expanding image panels we made together in this other article:
…and transform it into a zig-zag image panels! And this is only one example among the many we will discover in the next article.
If you’ve spent time looking at open-source repos on GitHub, you’ve probably noticed that most of them use badges in their README files. Take the official React repository, for instance. There are GitHub badges all over the README file that communicate important dynamic info, like the latest released version and whether the current build is passing.
Badges like these provide a nice way to highlight key information about a repository. You can even use your own custom assets as badges, like Next.js does in its repo.
But the most useful thing about GitHub badges by far is that they update by themselves. Instead of hardcoding values into your README, badges in GitHub can automatically pick up changes from a remote server.
Let’s discuss how to add dynamic GitHub badges to the README file of your own project. We’ll start by using an online generator called badgen.net to create some basic badges. Then we’ll make our badges dynamic by hooking them up to our own serverless function via Napkin. Finally, we’ll take things one step further by using our own custom SVG files.
First off: How do badges work?
Before we start building some badges in GitHub, let’s quickly go over how they are implemented. It’s actually very simple: badges are just images. README files are written in Markdown, and Markdown supports images like so:

The fact that we can include a URL to an image means that a Markdown page will request the image data from a server when the page is rendered. So, if we control the server that has the image, we can change what image is sent back using whatever logic we want!
Thankfully, we have a couple options to deploy our own server logic without the whole “setting up the server” part. For basic use cases, we can create our GitHub badge images with badgen.net using its predefined templates. And again, Napkin will let us quickly code a serverless function in our browser and then deploy it as an endpoint that our GitHub badges can talk to.
Making badges with Badgen
Let’s start off with the simplest badge solution: a static badge via badgen.net. The Badgen API uses URL patterns to create templated badges on the fly. The URL pattern is as follows:
Badgen provides a ton of different options, so I encourage you to check out their site and play around! For instance, one of the templates lets you show the number of times a given GitHub repo has been starred. Here’s a star GitHub badge for the Next.js repo as an example:
Pretty cool! But what if you want your badge to show some information that Badgen doesn’t natively support? Luckily, Badgen has a URL template for using your own HTTPS endpoints to get data:
https://badgen.net/https/url/to/your/endpoint
For example, let’s say we want our badge to show the current price of Bitcoin in USD. All we need is a custom endpoint that returns this data as JSON like this:
The data for the cost of Bitcoin is served right to the GitHub badge.
Even cooler now! But we still have to actually create the endpoint that provides the data for the GitHub badge. 🤔 Which brings us to…
Badgen + Napkin
There’s plenty of ways to get your own HTTPS endpoint. You could spin up a server with DigitalOcean or AWS EC2, or you could use a serverless option like Google Cloud Functions or AWS Lambda; however, those can all still become a bit complex and tedious for our simple use case. That’s why I’m suggesting Napkin’s in-browser function editor to code and deploy an endpoint without any installs or configuration.
Head over to Napkin’s Bitcoin badge example to see an example endpoint. You can see the code to retrieve the current Bitcoin price and return it as JSON in the editor. You can run the code yourself from the editor or directly use the endpoint.
To use the endpoint with Badgen, work with the same URL scheme from above, only this time with the Napkin endpoint:
Next, let’s fork this function so we can add in our own custom code to it. Click the “Fork” button in the top-right to do so. You’ll be prompted to make an account with Napkin if you’re not already signed in.
Once we’ve successfully forked the function, we can add whatever code we want, using any npm modules we want. Let’s add the Moment.js npm package and update the endpoint response to show the time that the price of Bitcoin was last updated directly in our GitHub badge:
Deploy the function, update your URL, and now we get this.
You might notice that the badge takes some time to refresh the next time you load up the README file over at GitHub. That’s is because GitHub uses a proxy mechanism to serve badge images.
GitHub serves the badge images this way to prevent abuse, like high request volume or JavaScript code injection. We can’t control GitHub’s proxy, but fortunately, it doesn’t cache too aggressively (or else that would kind of defeat the purpose of badges). In my experience, the TTL is around 5-10 minutes.
OK, final boss time.
Custom SVG badges with Napkin
For our final trick, let’s use Napkin to send back a completely new SVG, so we can use custom images like we saw on the Next.js repo.
A common use case for GitHub badges is showing the current status for a website. Let’s do that. Here are the two states our badge will support:
Badgen doesn’t support custom SVGs, so instead, we’ll have our badge talk directly to our Napkin endpoint. Let’s create a new Napkin function for this called site-status-badge.
The code in this function makes a request to example.com. If the request status is 200, it returns the green badge as an SVG file; otherwise, it returns the red badge. You can check out the function, but I’ll also include the code here for reference:
Odds are pretty low that the example.com site will ever go down, so I added the forceFail case to simulate that scenario. Now we can add a /400 after the Napkin endpoint URL to try it:
And there we have it! Your GitHub badge training is complete. But the journey is far from over. There’s a million different things where badges like this are super helpful. Have fun experimenting and go make that README sparkle! ✨