Sports! Go team!
Since I spend my days on the bottom of the JavaScript rabbit hole my CSS fu has seriously waned. It was therefore just the other day I was made aware of the existence of the CSS3 flexbox tech - holy crap, why didn't anyone tell me about this? This is seriously awesome!
While reading the excellent guide on CSSTricks I stumbled upon a really neat Codepen by developer Aron Duby, where he used flex to build a tournament bracket without having to resort to any other positioning. Here's what it looks like:
- Lousville 79
- NC A&T 48
- Colo St 84
- Missouri 72
- Oklahoma St 55
- Oregon 68
- Saint Louis 64
- New Mexico St 44
- Memphis 54
- St Mary's 52
- Mich St 65
- Valparaiso 54
- Creighton 67
- Cincinnati 63
- Duke 73
- Albany 61
- Lousville 82
- Colo St 56
- Oregon 74
- Saint Louis 57
- Memphis 48
- Mich St 70
- Creighton 50
- Duke 66
- Lousville 77
- Oregon 69
- Mich St 61
- Duke 71
- Lousville 85
- Duke 63
I was intrigued - there are several tough positional problems involved here, solved effortlessly with flex. What is this magic?
The code
I took the liberty of distilling the HTML down to its bare minimum:
<main>
<ul>
<li> </li>
<li class="game game-top winner">Lousville <span>79</span></li>
<li> </li>
<li class="game game-bottom ">NC A&T <span>48</span></li>
<li> </li>
<li class="game game-top winner">Colo St <span>84</span></li>
<li> </li>
<li class="game game-bottom ">Missouri <span>72</span></li>
<li> </li>
<!-- REDACTED SOME GAMES --->
<li class="game game-top winner">Duke <span>73</span></li>
<li> </li>
<li class="game game-bottom ">Albany <span>61</span></li>
<li> </li>
</ul>
<ul>
<!-- redacted, same structure as round 1 -->
</ul>
<ul>
<!-- redacted -->
</ul>
<ul>
<li> </li>
<li class="game game-top winner">Lousville <span>85</span></li>
<li> </li>
<li class="game game-bottom ">Duke <span>63</span></li>
<li> </li>
</ul>
</main>
And here's the magical CSS (adapted to the simpler HTML):
main,
ul {
display: flex;
}
ul {
flex-direction: column;
width: 200px;
list-style: none;
padding: 0;
}
.game + li {
flex-grow: 1;
}
li:first-child,
li:last-child {
flex-grow: 0.5;
}
.game {
padding-left: 20px;
}
.winner {
font-weight: bold;
}
.game span {
float: right;
margin-right: 5px;
}
.game-top {
border-bottom: 1px solid #aaa;
}
.game-top + li {
border-right: 1px solid #aaa;
min-height: 40px;
}
.game-bottom {
border-top: 1px solid #aaa;
}
The magic
All flex
boxes have a flex-direction
which is row
(default) or column
. The main
element is a flex row, and contains 4 ul
elements. The flex property align-items
defaults to stretch
, which means that they will all get the same height.
<main> <!-- flex row with align-items stretch -->
<ul> <!-- width 200px column. this first round has most markup and will dictate height of the others -->
<ul> <!-- width 200px column. will grow in height to match the first column -->
<!-- ...and so on... -->
</main>
Now for the interesting stuff - what's actually going on inside the columns? Here's how the rules will be applied:
<ul>
<!-- flex column -->
<li><!-- first and last li will be given flex-grow .5 --></li>
<li class="game-top"><!-- home team gets a border-bottom --></li>
<li>
<!-- li between teams gets min-height 40, a border-right and flex-grow 1 -->
</li>
<li class="game-bottom"><!-- away team gets border-top --></li>
<li>
<!-- li between games just gets flex-grow 1 -->
<!-- ...and so on... -->
</li>
</ul>
The elements with flex-grow
will be resized to give the container its expected height, which due to align-items
being set to stretch
should result in columns with equal height.
The value of flex-grow dictates how much the elements should grow in proportion to each other. This means the team spacers and the game spacer li
will all be the same height although the latter has a minimum of 40. The first and last li
will be half size, which is Aron's brilliant way of ensuring vertical centering inside the column without actually having to even use the (otherwise very powerful) justify-content
flex property.
If we had not put min-height:40px
on the spacers the first round would be totally squashed, since it due to having the tallest content is the one dictating the height of the other columns, and thus doesn't need to grow.
Spectral glasses
To make this more visible here's the bracket again but with the flex-grow:1
items marked in yellow and the flex-grow:0.5
items marked in blue:
- Lousville 79
- NC A&T 48
- Colo St 84
- Missouri 72
- Oklahoma St 55
- Oregon 68
- Saint Louis 64
- New Mexico St 44
- Memphis 54
- St Mary's 52
- Mich St 65
- Valparaiso 54
- Creighton 67
- Cincinnati 63
- Duke 73
- Albany 61
- Lousville 82
- Colo St 56
- Oregon 74
- Saint Louis 57
- Memphis 48
- Mich St 70
- Creighton 50
- Duke 66
- Lousville 77
- Oregon 69
- Mich St 61
- Duke 71
- Lousville 85
- Duke 63
The essence
So what did we actually do? In essence we used flex for two different things:
- By making the outer main element a flex row we used the default
stretch
value ofalign-items
to make sure the columns would get equal height. This would otherwise be difficult to do. - By then making each column a flex column, we used
flex-grow
to make sure the elements filled out the column height in the way we wanted. This would be really difficult by other means!
Note that for compatibility with Safari, iOS Safari and IE10 we have to use display: -webkit-flex
, -webkit-flex-direction: column
etc.
Pseudofying
Although already seriously neat, it still felt somewhat icky having to have the spacer li
elements in the markup. I tried to add them as pseudo elements using :before
and :after
, but couldn't get it to work. The reason is simple - despite their names, the pseudoclasses don't actually create the pseudo element before or after the target element, but inside them (at the top or bottom). That means the spacers wouldn't be siblings but cousins, and they wouldn't be children of the flex column. Thus the flex growth won't happen.
If there had been some css pseudo class equivalents of :before
and :after
that actually did create siblings to the target element, then it would have worked! But without that I don't see that it is possible to do the bracket without vilifying the markup.
The exception is the first and last spacer in the round - them we can create using pseudo classes on the columns! So we replace this rule...
li:first-child,
li:last-child {
flex-grow: 0.5;
}
...with this rule...
ul:before,
ul:after {
content: " ";
display: inline-block;
flex-grow: 0.5;
}
...and rip out the first and last spacer in each round...
<main>
<ul>
<li class="game game-top winner">Lousville <span>79</span></li>
<li> </li>
<li class="game game-bottom ">NC A&T <span>48</span></li>
<li> </li>
<li class="game game-top winner">Colo St <span>84</span></li>
<li> </li>
<li class="game game-bottom ">Missouri <span>72</span></li>
<li> </li>
<!-- REDACTED SOME GAMES --->
<li class="game game-top winner">Duke <span>73</span></li>
<li> </li>
<li class="game game-bottom ">Albany <span>61</span></li>
</ul>
<ul>
<!-- redacted, same structure as round 1 -->
</ul>
<ul>
<!-- redacted -->
</ul>
<ul>
<li class="game game-top winner">Lousville <span>85</span></li>
<li> </li>
<li class="game game-bottom ">Duke <span>63</span></li>
</ul>
</main>
...and we still get the same result as before:
- Lousville 79
- NC A&T 48
- Colo St 84
- Missouri 72
- Oklahoma St 55
- Oregon 68
- Saint Louis 64
- New Mexico St 44
- Memphis 54
- St Mary's 52
- Mich St 65
- Valparaiso 54
- Creighton 67
- Cincinnati 63
- Duke 73
- Albany 61
- Lousville 82
- Colo St 56
- Oregon 74
- Saint Louis 57
- Memphis 48
- Mich St 70
- Creighton 50
- Duke 66
- Lousville 77
- Oregon 69
- Mich St 61
- Duke 71
- Lousville 85
- Duke 63
Wrapping up
I thoroughly enjoyed picking apart Aron's neat codepen, and adding flex to my toolbelt felt like it levelled up my CSS fu several times over. The already mentioned CSSTricks guide really is very good, so if you've been underground for as long as me and want to catch up, go check it out!
Still, I wonder if there really is no way to create the tournament bracket without having to resort to markup spacers. This really feels like the girl who got away! If YOU are sitting on the secret, do get in touch!