Responsive gallery tiles with pure CSS

Airships!
7 min readJul 22, 2017

--

Hello world! I’m not very familiar with blogging nor Medium, so… Sorry if it’s hard to read or something :)

When I joined SoFurry-Next team last spring, I noticed they had an old problem with gallery view. The problem was with creation of perfectly aligned tiles for gallery views, including text posts. Masonry JS? Otherthingy JS? Yes, but they weren’t the perfect solution at all, since we want to avoid JS as much as possible. When I said that I could try to make it with pure CSS, no one believed me. Challenge accepted!

Final result

I knew how to make Pinterest-like tiles with CSS float for a long time, but this approach aren’t good for this task, since we want horizontal rows, not vertical. For the last years I was having fun with CSS flexbox, slightly abusing it for one big game company web project. So my solution was a flexbox layout.

Here’s a great flexbox reference:

Please note, that I expect reader to be familiar with CSS and related wizardry :)

JSFiddle result can be found at the end of this post.

Task requirements

  • Tiles could be image or text (HTML).
  • Both tiles should include text title and must be clickable.
  • Cosmetics: tiles should be zoomable on hover.
  • Size of tiles’ source is unknown (sic).
  • Tiles must fill all of the available space in a container, depending on their content.
  • Container could be resized.
  • No JS if possible (Spoiler: possible!)
  • Cross-platform (I used IE11 during development, so other browsers work fine with the result)

Let’s start

We start with a typical HTML5 code with some JS as a content generator. JS isn’t required, but it eases the template modifications right now. On a final website for this task HTML gets generated with PHP (which isn’t my job).

<!DOCTYPE html>
<html>
<head>
<style>
* {
box-sizing: border-box;
}
</style>
<title>Tiles that always fit! (CSS3)</title>
</head>
<body>

<div id="thumbs" class="thumbs"></div>

<script>
// scripted html generator to ease the demo creation
// note that it generates only html, no sizes or anything else

var data = [
{type: "img", src: "https://i.imgur.com/pXpH4tem.jpg", href: "#somelink", title: "Pidgins", text: "by me"},
{type: "img", src: "https://i.imgur.com/LbrZV1fm.jpg", href: "#somelink", title: "White doge eyes", text: "by me"},
{type: "img", src: "https://i.imgur.com/PwaC6O4m.jpg", href: "#somelink", title: "Three doges", text: "by me"},
{type: "img", src: "https://i.imgur.com/gUbHmiFm.jpg", href: "#somelink", title: "Doge", text: "by me"},
{type: "img", src: "https://i.imgur.com/siIy4H8m.jpg", href: "#somelink", title: "White doge", text: "by me"},
{type: "img", src: "https://i.imgur.com/q3G3OTSm.jpg", href: "#somelink", title: "Two doges", text: "by me"},
{type: "img", src: "https://i.imgur.com/ZR6LWClm.jpg", href: "#somelink", title: "Graffiti", text: "by me"},
{type: "img", src: "https://i.imgur.com/B73Q1pWm.jpg", href: "#somelink", title: "High-rise", text: "by me"},
{type: "img", src: "https://i.imgur.com/mljsP5Um.jpg", href: "#somelink", title: "High-rise with bird", text: "by me"},
{type: "img", src: "https://i.imgur.com/SVPCKnmm.jpg", href: "#somelink", title: "WoW", text: "by me"},
{type: "img", src: "https://i.imgur.com/27BQP5Om.jpg", href: "#somelink", title: "WoW", text: "by me"},
{type: "txt", href: "#somelink", title: "Moo!", text: "Says cow. They also known to have a milk for everyone. Fun fact: you can feed a cow with chocolate to have a chocolate milk!"},
{type: "img", src: "https://i.imgur.com/WrLXGvem.jpg", href: "#somelink", title: "Sergals", text: "by me"},
{type: "img", src: "https://i.imgur.com/abGV4R8m.jpg", href: "#somelink", title: "Sergals", text: "by me"},
{type: "txt", href: "#somelink", title: "Merp!", text: "Says sergal."}
];

// html templates
// image
var tplimg = '<a href="%href%" class="thumb thumb-img" style="background-image: url(%src%)">'
+ '<img src="%src%" alt="%title%"/>'
+ '<div><span>%title%</span><span>%text%</span></div>'
+ '</a>';
// text
var tpltxt = '<a href="%href%" class="thumb thumb-txt">'
+ '<div><span>%title%</span><span>%text%</span></div>'
+ '</a>';

var cnt = document.getElementById("thumbs");
var cs = "";
for (var i=0; i<data.length; i++) {
// choose a template to use
var tpl = data[i].type == "img" ? tplimg : tpltxt;
// replace strings
for (var k in data[i])
tpl = tpl.replace(eval("/%"+k+"%/g"), data[i][k]);
// save the result
cs += tpl;
}
cnt.innerHTML = cs;
</script>

</body>
</html>

What we get is HTML with some sample content. Templates in this example:

<!-- Image -->
<a href="https://example.com" class="thumb thumb-img" style="background-image: url(https://example.com/img/example.jpg)">
<img src="https://example.com/img/example.jpg" alt=""/>
<div>
<span>Example title</span>
<span>Example subtext</span>
</div>
</a>
<!-- /Image -->
<!-- Text -->
<a href="https://example.com" class="thumb thumb-txt">
<div>
<span>Example title</span>
<span>Example content</span>
</div>
</a>
<!-- /Text -->

For reference, classes are:

  • .thumbs for the container
  • Shared .thumb class for items
  • Individual .thumb-img and .thumb-txt classes for image and text tiles

Item’s parent is <a>, which isn’t really correct use of anchor, but hey, it works well for no-js solution. Inside of it we see <div> with text title and subtext/content <span>s, and <img> for image tile (we’ll get back to this one later).

CSS

Of course, it will be CSS3. So let’s start with container.

.thumbs {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-content: flex-start;
align-items: stretch;
/* for demo: */
width: 650px;
border: 1px solid #ccc;
margin: 10px auto;
resize: both;
}

Container’s job is to keep everything stretched in rows inside it.

Shared tile rules:

.thumb {
position: relative;
display: inline-block;
flex: 1 0 auto;
width: auto;
height: 120px;
/* cosmetics and zoom: */
margin: 2px;
text-decoration: none;
background-color: rgba(0,0,0, 0.50);
box-shadow: 0px 0px 0px 0px rgba(0,0,0, 0.0);
transition: box-shadow 300ms, transform 400ms, z-index 400ms, background-position 400ms;
z-index: 1;
}
.thumb:hover {
/* zoom: */
box-shadow: 0px 0px 10px 0px rgba(0,0,0, 0.7);
transform: scale(1.2, 1.2);
z-index: 100;
}

To keep everything perfectly aligned in one row, we should not forget to specify height.

Hover, transition, transform and z-index here are for zoom. Z-index is vital to keep zoomed tiles above their neighbours. Don’t forget to specify z-index in both rules or this won’t work in all browsers.

Transition for background-position is only used for image tiles, we’ll get back to this later.

Limit min and max sizes for tiles. They can be separate, but for a demo let’s make them the same.

.thumb-img > img,
.thumb-txt > div {
min-width: 60px;
max-width: 300px;
}

Remember that we have requirement of tile size by its content, BUT they should cover the whole row? That’s exactly why we have to set the sizes for inside elements — to allow them to overstretch.

Image tile:

.thumb-img {
background-size: cover;
background-position: 50% 50%;
background-repeat: no-repeat;
}
.thumb-img:hover {
animation-name: thumb-scroll;
animation-delay: 400ms;
animation-duration: 2.5s;
animation-iteration-count: infinite;
animation-direction: normal;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.thumb-img > img {
display: block;
height: 100%;
opacity: 0;
}
@keyframes thumb-scroll {
15%, 35% { background-position: 0% 0%; }
65%, 85% { background-position: 100% 100%; }
}

We have image template with background-image set in embed style for the parent element, and that’s why:

  • <img> element is used only to set the base tile size and proportions, not to be visible!
  • Parent’s background-image is what we will show in a tile. But to be sure if it is shown on the whole tile’s space, we should sacrifice some of it’s height. In other words, when the tile gets wider than width by its content’s proportions, the image inside it becomes stretched by width forcing vertical image parts out of screen. This can’t be solved in other way, so we will deal with it using:
  • :hover and keyframes. They are used to scroll the image up and down to show the whole picture. And that’s where .thumbs transition background-position is used. Easy, huh?
Notice the correct zooming overlap — z-index is in the game!

Now we add text styles. They are used by both image and text tiles.

.thumb > div {
font-family: "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 14px;
color: #fff;
padding: 0px 2px;
overflow: hidden;
}
.thumb > div > span {
display: block;
}
.thumb > div > span:first-child { /* title */
font-weight: bold;
}
.thumb > div > span:last-child { /* subtext */
font-size: 0.8em;
}

And the rest of the text styles, including small part for the text tile:

.thumb-img > div {
display: block;
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
white-space: nowrap;
background-color: rgba(0,0,0, 0.30);
opacity: 0;
transition: opacity 300ms;
}
.thumb-img > div > span { /* title */
font-size: 0.8em;
}
.thumb-img:hover > div { /* content */
opacity: 1;
}

.thumb-txt > div {
display: block;
width: auto;
height: 100%;
}

To avoid lone items stretching for the whole row (could happen with the last row), add this rule:

.thumbs:after {
content: "";
flex: 100 0 auto;
}

This will add blank space at the end, which will appear only when needed. Alas, this may not work in IE :(

That’s all, folks!

--

--

Airships!
Airships!

Written by Airships!

Web/UI/UX full-stack developer for ~20yr

Responses (1)