Sometimes flex-direction: row is better for columns

I was recently working on a project, when I found an interesting situation with CSS where flex-direction: row is a better fit for a column based arrangement than flex-direction: column.

In this example, we would like to have this box in the middle of the page horizontally, but also with the width defined by the number of buttons at the bottom, with the title and text box changing size with it.

On the surface, this is a simple problem to solve with flexbox, but alas, it is not.

This is the code you’d typically use for a problem like this:

<!-- CONTAINER -->
<div style="display: flex; min-height: 100vh; align-items: center; justify-content: center">
	<!-- BOX -->
	<div style="display: inline-flex; flex-direction: column;">
		<!-- TITLE -->
		<h3 style="width: 100%">Lorem ipsum dolor sit amet consecuteur blah blah blah blah</h3>
		<!-- TEXT BOX -->
		<div style="width: 100%">
			<input style="width: 100%" value="Text box " autofocus>
		</div>
		<!-- BUTTONS -->
		<div style="flex-shrink: 0">
			<button>A</button>
			<button>B</button>
			<button>C</button>
			<button>D</button>
		</div>
	</div>
</div>

The rationale is:

  • use the flex-direction column layout for the box; a column of 3 rows
  • use an inline-flex for the box so it is only as wide as it needs to be to contain the rows
  • width: 100% on the other columns should define them as the width of the container, and therefore neither can be the widest row in the box
  • flex-shrink: 0 on the buttons row should force the box to treat them as the widest row, as they cannot shrink while the others, with the default value of 1, can

However, when set up with this, it ends up looking like this:

However, it seems that the inline-flex box is defining its own width from the width of the whole line of text; why?

This is similar to why setting height: 100% doesn’t do anything when the container’s height is auto; and inline-flex is similar to this on the width axis.

That means that the width of the box is still defined as the width of the widest row, which is the text here.

When we hard code the width of the top two rows to the width in pixels of the buttons row, we get this:

Yay!

Except, of course, it’s hardcoded. When we add an extra button, this no longer works:

Interestingly, align-items: stretch is not a perfect cross-axis analogue of flex-grow: 1, as stretch obeys a fixed size, and flex-grow does not (though it does obey max-width/max-height).

So given that flex-grow can grow beyond the “auto” width; does that mean that it can solve this problem?

Spoiler: yes

It requires an extra wrapper div around the rows that we’d like to be flexible, but indeed it can be done, as shown below:

<!-- CONTAINER -->
<div style="display: flex; width: 100%; min-height: 100vh; align-items: center; justify-content: center;">
	<!-- BOX -->
	<div style="display: inline-flex; flex-wrap: wrap;">
	<!-- EXTRA WRAPPER DIV -->
		<div style="width: 1px; flex: 1 0 auto">
			<!-- TITLE -->
			<h3 style="width: 100%">
				Lorem ipsum dolor sit amet consecuteur blah blah blah
			</h3>
			<!-- TEXT BOX -->
			<div style="width: 100%">
				<input style="width: 100%" value="Text box " autofocus>
			</div>
		</div>
		<!-- BUTTONS -->
		<div style="width: 100%; flex: 0 0 auto">
			<button>
				A
			</button>
			<button>
				B
			</button>
			<button>
				C
			</button>
			<button>
				D
			</button>
		</div>
	</div>
</div>

And the result:

How does it work?

The key with this solution is to use width: 1px to trick the inline-flex box into thinking that the widest column is the buttons, and then use flex-grow: 1 (here flex: 1 0 auto) to grow from auto (i.e. 1px) to fit the space.

To push the button row down, the box needs to be flex-wrap: wrap and the button row needs to have width: 100%; which is used here to force it to be on it’s own row; though as we discovered before with percentage widths and display: inline-flex, this doesn’t actually change its own width beyond this.

Conclusion

Sometimes flex-direction: row is better suited than flex-direction: column for columns. Go figure… 🤷