22

I have a horizontal flex box (i.e. flex-direction: row, i.e. side-by-side) with a few items. Each item can be a single line of text, or can have multiple lines. I want to vertically-align the contents of each flex item.

If each item had a transparent background, I could easily use align-items: center. However, I want each item to be stretched vertically, because I want to set a background (or maybe borders, or maybe it is a clickable region) to the entire available height.

So far, I know:

  • Stretching: align-items: stretch
  • Aligning: align-items: center
  • Stretching and aligning: ???

Demo available at http://codepen.io/denilsonsa/pen/bVBQNa

Screenshot of the demo

ul {
  display: flex;
  flex-direction: row;
}
ul.first {
  align-items: stretch;
}
ul.second {
  align-items: center;
}
ul > li {
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: 5em;
  text-align: center;
}
ul > li:nth-child(2) {
  background: #CFC;
}

/* Visual styles, just ignore. */
html, body { font-family: sans-serif; font-size: 25px; }
ul, li { list-style: none; margin: 0; padding: 0; }
ul { background: #CCF; width: 25em; }
<ul class="first">
  <li>Sample</li>
  <li><span>span</span></li>
  <li><span>multiple</span> <span>span</span></li>
  <li>text <span>span</span></li>
  <li>multi<br>line</li>
</ul>
<hr>
<ul class="second">
  <li>Sample</li>
  <li><span>span</span></li>
  <li><span>multiple</span> <span>span</span></li>
  <li>text <span>span</span></li>
  <li>multi<br>line</li>
</ul>

Similar questions:

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
Denilson Sá Maia
  • 47,466
  • 33
  • 109
  • 111

4 Answers4

8

Unfortunately, it is impossible to achieve the desired effect while using the exact markup posted in the question.

The solution involves:

  • Setting display: flex; on <li>.
  • Wrapping the <li> contents into another element.
    • This is required because <li> is now a flex container, so we need another element to prevent the actual contents from becoming flex items.
    • In this solution, I introduced a <div> element, but it could have been other element.
  • Now that <li> is a flex container and it contains only a single child, we can use align-items and/or justify-content to align this new and only child.

The DOM tree looks like this:

<ul> flex-parent, direction=row
 ├ <li> flex-item && flex-parent && background && JavaScript clickable area
 │  └ <div> flex-item as a single transparent element
 │     ├ Actual contents
 │     └ Actual contents
 ├ …

Note: The solution in this answer uses 2 nested flex boxes. The solution by Michael_B uses 3 nested flex boxes, because it has the added challenge of expanding the <a> element to fill the entire <li>. Which one is preferred depends on each case. If I could, I would accept both answers.

/* New code: this is the solution. */
ul > li {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

/* Old code below. */
ul {
  display: flex;
  flex-direction: row;
  align-items: stretch;
}
ul > li {
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: 5em;
  text-align: center;
}
ul > li:nth-child(2) {
  background: #CFC;
}

/* Visual styles, just ignore. */
html, body { font-family: sans-serif; font-size: 25px; }
ul, li { list-style: none; margin: 0; padding: 0; }
ul { background: #CCF; width: 25em; }

button:focus + ul {
    font-size: 14px;
    width: auto;
}
<button>Click here to set <code>width: auto</code> and reduce the font size.</button>

<!-- New code: there is a single <div> between each <li> and their contents. -->
<ul>
  <li><div>Sample</div></li>
  <li><div><span>span</span></div></li>
  <li><div><span>multiple</span> <span>span</span></div></li>
  <li><div>text <span>span</span></div></li>
  <li><div>multi<br>line</div></li>
</ul>
Denilson Sá Maia
  • 47,466
  • 33
  • 109
  • 111
7

I want each item to be stretched vertically, because I want to set a background (or maybe borders, or maybe it is a clickable region) to the entire available height.

You can achieve this layout without any changes to your HTML structure. There's no need for additional containers.

You already have a primary flex container and a group of flex items. Simply make those flex items into nested flex containers. That will enable you to align the content with flex properties.

(Since you mentioned that you may need clickable regions, I switched from li to a elements.)

nav {
  display: flex;
  background: #CCF;
  width: 25em;
}

nav > a {
  flex: auto;  /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
  text-align: center;
  text-decoration: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
nav > a:nth-child(2) {
  background: #CFC;
}

html, body {
  font-family: sans-serif;
  font-size: 25px;
}
<nav>
  <a href="">Sample</a>
  <a href=""><span>span</span></a>
  <a href=""><span>multiple</span> <span>span</span></a>
  <a href="">text <span>span</span></a>
  <a href="">multi<br>line</a>
</nav>

revised codepen

Note that content placed directly inside a flex container is wrapped in an anonymous flex item:

From the spec:

4. Flex Items

Each in-flow child of a flex container becomes a flex item, and each contiguous run of text that is directly contained inside a flex container is wrapped in an anonymous flex item.

So, because the text is automatically wrapped in flex items, you can keep the full height of each item (align-items: stretch from the primary container) and vertically center the content (align-items: center from the nested containers).

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701