A List Apart

Menu
Illustration: A bird's eye view of a table with different dishes, chairs, and place settings in each spot.

Illustration by Dougal MacPherson

Faux Grid Tracks

A little while back, there was a question posted to css-discuss:

Is it possible to style the rows and columns of a [CSS] grid—the grid itself? I have an upcoming layout that uses what looks like a tic-tac-toe board—complete with the vertical and horizontal lines of said tic-tac-toe board—with text/icon in each grid cell.
Article Continues Below

This is a question I expect to come up repeatedly, as more and more people start to explore Grid layout. The short answer is: no, it isn’t possible to do that. But it is possible to fake the effect, which is what I’d like to explore.

Defining the grid

Since we’re talking about tic-tac-toe layouts, we’ll need a containing element around nine elements. We could use an ordered list, or a paragraph with a bunch of <span>s, or a <section> with some <div>s. Let’s go with that last one.

<section id="ttt">
	<div>1</div>
	<div>2</div>
	<div>3</div>
	<div>4</div>
	<div>5</div>
	<div>6</div>
	<div>7</div>
	<div>8</div>
	<div>9</div>
</section>

We’ll take those nine <div>s and put them into a three-by-three grid, with each row five ems high and each column five ems wide. Setting up the grid structure is straightforward enough:

#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}

That’s it! Thanks to the auto-flow algorithm inherent in Grid layout, that’s enough to put the nine <div> elements into the nine grid cells. From there, creating the appearance of a grid is a matter of setting borders on the <div> elements. There are a lot of ways to do this, but here’s what I settled on:

#ttt > * {
	border: 1px solid black;
	border-width: 0 1px 1px 0;
	display: flex; /* flex styling to center content in divs */
	align-items: center;
	justify-content: center;
}
#ttt > *:nth-of-type(3n)  {
	border-right-width: 0;
}
#ttt > *:nth-of-type(n+7) {
	border-bottom-width: 0;
}

The result is shown in the basic layout below.

Screenshot: The basic layout features a 3x3 grid with lines breaking up the grid like a tic-tac-toe board.
Figure 1: The basic layout

This approach has the advantage of not relying on class names or what-have-you. It does fall apart, though, if the grid flow is changed to be columnar, as we can see in Figure 2.

#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
	grid-auto-flow: column;  /* a change in layout! */
}
Screenshot: If you switch the grid to columnar flow order, the borders get out of whack. Instead of a tic-tac-toe board, the right-most horizontal borders have moved to the bottom of the grid and the bottom-most vertical borders have moved to the right edge.
Figure 2: The basic layout in columnar flow order

If the flow is columnar, then the border-applying rules have to get flipped, like this:

#ttt > *:nth-of-type(3n) {
	border-bottom-width: 0;
}
#ttt > *:nth-of-type(n+7) {
	border-right-width: 0;
}

That will get us back to the result we saw in Figure 1, but with the content in columnar order instead of row order. There’s no row reverse or column reverse in Grid like there is in flexbox, so we only have to worry about normal row and columnar flow patterns.

But what if a later change to the design leads to grid items being rearranged in different ways? For example, there might be a reason to take one or two of the items and display them last in the grid, like this:

#ttt > *:nth-of-type(4), #ttt > *:nth-of-type(6) {
	order: 66;
}

Just like in flexbox, this will move the displayed grid items out of source order, placing them after the grid items that don’t have explicit order values. If this sort of rearrangement is a possibility, there’s no easy way to switch borders on and off in order to create the illusion of the inner grid lines. What to do?

Attack of the filler <b>s!

If we want to create standalone styles that follow grid tracks—that is, presentation aspects that aren’t directly linked to the possibly-rearranged content—then we need other elements to place and style. They likely won’t have any content, making them a sort of structural filler to spackle over the gaps in Grid’s capabilities.

Thus, to the <section> element, we can add two <b> elements with identifiers.

<section id="ttt">
	<b id="h"></b>
	<b id="v"></b>
	<div>1</div>
	<div>2</div>
	<div>3</div>
…

These “filler <b>s,” as I like to call them, could be placed anywhere inside the <section>, but the beginning works fine. We’ll stick with that. Then we add these styles to our original grid from the basic layout:

b[id] {
	border: 1px solid gray;
}
b#h {
	grid-column: 1 / -1;
	grid-row: 2;
	border-width: 1px 0;
}
b#v {
	grid-column: 2;
	grid-row: 1 / -1;
	border-width: 0 1px;
}

The 1 / -1 means “go from the first grid line to the last grid line of the explicit grid”, regardless of how many grid lines there might be. It’s a handy pattern to use in any situation where you have a grid item meant to stretch from edge to edge of a grid.

So the horizontal <b> has top and bottom borders, and the vertical <b> has left and right borders. This creates the board lines, as shown in Figure 3.

Screenshot: With the filler b tags, you can see the tic-tac-toe board again. But only the corners of the grid are filled with content, and there are 5 cells below the board as the grid lines have displaced the content.
Figure 3: The basic layout with “Filler <b>s”

Hold on a minute: we got the tic-tac-toe grid back, but now the numbers are in the wrong places, which means the <div>s that contain them are out of place. Here’s why: the <div> elements holding the actual content will no longer auto-flow into all the grid cells, because the filler <b>s are already occupying five of the nine cells. (They’re the cells in the center column and row of the grid.) The only way to get the <div> elements into their intended grid cells is to explicitly place them. This is one way to do that:

div:nth-of-type(3n+1) {
	grid-column: 1;
}
div:nth-of-type(3n+2) {
	grid-column: 2;
}
div:nth-of-type(3n+3) {
	grid-column: 3;
}
div:nth-of-type(-n+3) {
	grid-row: 1;
}
div {
	grid-row: 2;
}
div:nth-of-type(n+7) {
	grid-row: 3;
}

That works if you know the content will always be laid out in row-then-column order. Switching to column-then-row requires rewriting the CSS. If the contents are to be placed in a jumbled-up order, then you’d have to write a rule for each <div>.

This probably suffices for most cases, but let’s push this even further. Suppose you want to draw those grid lines without interfering with the automatic flow of the contents. How can this be done?

Overgridding

It would be handy if there were a property to mark elements as not participating in the grid flow, but there isn’t. So instead, we’ll split the contents and filler into their own grids, and use a third grid to put one of those grids over the other.

This will necessitate a bit of structural change to make happen, because for it to work, the contents and the filler <b>s have to have identical grids. Thus we end up with:

<section id="ttt">
	<div id="board">
		<b id="h"></b>
		<b id="v"></b>
	</div>
	<div id="content">
		<div>1</div>
		<div>2</div>
		<div>3</div>
		<div>4</div>
		<div>5</div>
		<div>6</div>
		<div>7</div>
		<div>8</div>
		<div>9</div>
	</div>
</section>

The first thing is to give the board and the content <div>s identical grids. The same grid we used before, in fact. We just change the #ttt rule’s selector a tiny bit, to select the children of #ttt instead:

#ttt > * {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}

Now that the two grids have the same layout, we need to place one over the other. We could relatively position the #ttt container and absolutely position its children, but there’s another way: use Grid.

#ttt { /* new rule added */
	display: grid;
}
#ttt > * {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}

But wait—where are the rows and columns for #ttt? Where we’re going, we don’t need rows (or columns). Here is how the two grids end up occupying the same area with one on top of the other:

#ttt {
	display: grid;
}
#ttt > * {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
	grid-column: 1;  /* explicit grid placement */
	grid-row: 1;  /* explicit grid placement */
}

So #ttt is given a one-cell grid, and its two children are explicitly placed in that single cell. Thus one sits over the other, as with positioning—but unlike positioning, the outer grid’s size is dictated by the layout of its children. It will resize to surround them, even if we later change the inner grids to be larger (or smaller). We can see this in practice in Figure 4, where the outer grid is outlined in purple in Firefox’s Grid inspector tool.

Screenshot: In the Firefox Grid Inspector, the containing grid spans the full width of the page with a purple border. Occupying about a third of the space on the left side of the container are the two child grids, one with the numbers 1 through 9 in a 3 by 3 grid and the other with tic-tac-toe lines overlaid on top of each other.
Figure 4: The overgridded layout

And that’s it. We could take further steps, like using z-index to layer the board on top of the content (by default, the element that comes later in the source displays on top of the element that comes earlier), but this will suffice for the case we have here.

The advantage is that the content <div>, having only its own contents to worry about, can make use of grid-auto-flow and order to rearrange things. As an example, you can do things like the following and you won’t need all of the :nth-of-type grid item placements from our earlier CSS. Figure 5 shows the result.

/* added to #snippet13 code */
#ttt > #content {
	grid-auto-flow: column;
}
#ttt > #content > :nth-child(5) {
	order: 2;
}
Screenshot: The overgridded version, where the numbered 3 by 3 grid is overlaid on top of the tic-tac-toe board, continues to work fine if you reorder the cells. In this case, the number 5 has moved from the central grid cell to the bottom right.
Figure 5: Moving #5 to the end and letting the other items reflow into columns

Caveats

The downside here, and it’s a pretty big one, is that the board and content grids are only minimally aware of each other. The reason the previous example works is the grid tracks are of fixed size, and none of the content is overflowing. Suppose we wanted to make the columns and rows resize based on content, like this:

#content {
	grid-template-columns: repeat(3,min-content);
	grid-template-rows: repeat(3,min-content);
}

This will fall apart quickly, with the board lines not corresponding to the layout of the actual content. At all.

In other words, this overlap technique sacrifices one of Grid’s main strengths: the way grid cells relate to other grid cells. In cases where content size is predictable but ordering is not, it’s a reasonable trade-off to make. In other cases, it probably isn’t a good idea.

Bear in mind that this really only works with layouts where sizes and placements are always known, and where you sometimes have to layer grids on top of one another. If your Filler <b> comes into contact with an implicitly-placed grid item in the same grid as it occupies, it will be blocked from stretching. (Explicitly-placed grid items, i.e., those with author-declared values for both grid-row and grid-column, do not block Filler <b>s.)

Why is this useful?

I realize that few of us will need to create a layout that looks like a tic-tac-toe board, so you may wonder why we should bother. We may not want octothorpe-looking structures, but there will be times we want to style an entire column track or highlight a row.

Since CSS doesn’t (yet) offer a way to style grid cells, areas, or tracks directly, we have to stretch elements over the parts we want to style independently from the elements that contain content. There is a discussion about adding this capability directly to CSS in the Working Group’s GitHub repository, where you can add your thoughts and proposals.

But why <b>s? Why?

I use <b>s for the decorative portions of the layout because they’re purely decorative elements. There’s no content to strongly emphasize or to boldface, and semantically a <b> isn’t any better or worse than a <span>. It’s just a hook on which to hang some visual effects. And it’s shorter, so it minimizes page bloat (not that a few characters will make all that much of a difference).

More to the point, the <b>’s complete lack of semantic meaning instantly flags it in the markup as being intentionally non-semantic. It is, in that meta sense, self-documenting.

Is this all there is?

There’s another way to get this precise effect: backgrounds and grid gaps. It comes with its own downsides, but let’s see how it works first. First, we set a black background for the grid container and white backgrounds for each item in the grid. Then, by using grid-gap: 1px, the black container background shows between the grid items.

<section id="ttt">
	<div>1</div>
	<div>2</div>
	<div>3</div>
	<div>4</div>
	<div>5</div>
	<div>6</div>
	<div>7</div>
	<div>8</div>
	<div>9</div>
</section>
#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
	background: black;
	grid-gap: 1px;
}
#ttt > div {
	background: white;
}

Simple, no Filler <b>s needed. What’s not to like?

The first problem is that if you ever remove an item, there will be a big black block in the layout. Maybe that’s OK, but more likely it isn’t. The second problem is that grid containers do not, by default, shrink-wrap their items. Instead, they fill out the parent element, as block boxes do. Both of these problems are illustrated in Figure 6.

Screenshot: When a grid cell goes missing with the background and grid-gap solution, it leaves a big black box in its place. There's also a giant black box filling the rest of the space to the right of the grid cells.
Figure 6: Some possible background problems

You can use extra CSS to restrict the width of the grid container, but the background showing through where an item is missing can’t really be avoided.

On the other hand, these problems could become benefits if, instead of a black background, you want to show a background image that has grid items “punch out” space, as Jen Simmons did in her “Jazz At Lincoln Center Poster” demo.

A third problem with using the backgrounds is when you just want solid grid lines over a varied page background, and you want that background to show through the grid items. In that case, the grid items (the <div>s in this case) have to have transparent backgrounds, which prevents using grid-gap to reveal a color.

If the <b>s really chap your cerebellum, you can use generated content instead. When you generate before- and after-content pseudo-elements, Grid treats them as actual elements and makes them grid items. So using the simple markup used in the previous example, we could write this CSS instead:

#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}
#ttt::before {
	grid-column: 1 / -1;
	grid-row: 2;
	border-width: 1px 0;
}
#ttt::after {
	grid-column: 2;
	grid-row: 1 / -1;
	border-width: 0 1px;
}

It’s the same as with the Filler <b>s, except here the generated elements draw the grid lines.

This approach works just fine for any 3x3 grid like the one we’ve been playing with, but to go any further, you’ll need to get more complicated. Suppose we have a 5x4 grid instead of a 3x3. Using gradients and repeating, we can draw as many lines as needed, at the cost of more complicated CSS.

#ttt {
	display: grid;
	grid-template-columns: repeat(5,5em);
	grid-template-rows: repeat(4,5em);
}
#ttt::before {
	content: "";
	grid-column: 1 / -1;
	grid-row: 1 / -2;
	background:
		linear-gradient(to bottom,transparent 4.95em, 4.95em, black 5em)
		top left / 5em 5em;
}
#ttt::after {
	content: "";
	grid-column: 1 / -2;
	grid-row: 1 / -1;
	background:
		linear-gradient(to right,transparent 4.95em, 4.95em, black 5em)
		top left / 5em 5em;
}

This works pretty well, as shown in Figure 7, assuming you go through the exercise of explicitly assigning the grid cells similar to how we did in #snippet9.

Screenshot: A 5 by 4 grid with evenly spaced borders dividing the cells internally using background gradients.
Figure 7: Generated elements and background gradients

This approach uses linear gradients to construct almost-entirely transparent images that have just a 1/20th of an em of black, and then repeating those either to the right or to the bottom. The downward gradient (which creates the horizontal lines) is stopped one gridline short of the bottom of the container, since otherwise there would be a horizontal line below the last row of items. Similarly, the rightward gradient (creating the vertical lines) stops one column short of the right edge. That’s why there are -2 values for grid-column and grid-row.

One downside of this is the same as the Filler <b> approach: since the generated elements are covering most of the background, all the items have to be explicitly assigned to their grid cells instead of letting them flow automatically. The only way around this is to use something like the overgridding technique explored earlier. You might even be able to drop the generated elements if you’re overgridding, depending on the specific situation.

Another downside is that if the font size ever changes, the width of the lines can change. I expect there’s a way around this problem using calc(), but I’ll leave that for you clever cogs to work out and share with the world.

The funny part to me is that if you do use this gradient-based approach, you’re filling images into the background of the container and placing items over that … just as we did with Faux Columns.

Conclusion

It’s funny how some concepts echo through the years. More than a decade ago, Dan Cederholm showed us how to fake full-height columns with background images. Now I’m showing you how to fake full-length column and row boxes with empty elements and, when needed, background images.

Over time, the trick behind Faux Columns fell out of favor, and web design moved away from that kind of visual effect. Perhaps the same fate awaits Faux Grid Tracks, but I hope we see new CSS capabilities arise that allow this sort of effect without the need for trickery.

We’ve outgrown so many of our old tricks. Here’s another to use while it’s needed, and to hopefully one day leave behind.

3 Reader Comments

Load Comments