in the first code snippet below I show a layout using flex-flow: row wrap
which has desirable layout properties but can exceed the maximum specified height of the container. The second code snippet shows a layout with flex-flow: column nowrap
that accomplishes the desired behaviour to keep the total height of content to fit on one screen but for a single column.
I would like a layout that combines the desirable properties of both, specifically I'd like to accomplish these goals:
- all elements within the container will have equal widths which is either
30px
or20vw
which ever is smaller. When there are more elements than fit on a single row they wrap to a new row, there are very often more than a single row as I typically have 7 to 16 tubes. - the elements all have (quantized and statically specified) arbitrary heights, rows should be tall enough for their tallest element and won't necessarily be all the same height.
- if the final result (whole container, typically multiple rows) has a height that exceeds some maximum like
90vh
it should shrink all rows by equal proportion to fit into the specified maximum height. (like they hadflex-shrink: 1
property in the vertical direction)
I personally will not accept an answer that uses javascript to set styling but you are welcome to post one to help people with similar problems but less strict requirements. I would prefer a pure CSS solution that gets close enough than a solution that works perfectly so long as you setup all the right listeners.
My current layout
The following is the layout I am using presently, there is a #gameContainer
which contains a list of .Tube
s which each contain some .Ball
s. The variable --ball-size
is set on the root container to make all the tube widths the same, each tube has a --capacity
property to specify it's height as a multiple of --ball-size
.
If you change the width of the main container and/or the ball size you will see that if it would exceed the specified max-height: 90vh
on the main container it overflows overlapping with the footer content (setting the width to 140
accomplishes this for me but may differ if your viewing window is different size)
/** * sets a style property of the #gameContainer * the property set is the `name` field of the given input * and the value is the value from the input with the specified suffix. */function updateGameContainerStyle(inputForm, suffix=""){document.getElementById("gameContainer").style.setProperty(inputForm.name, inputForm.value + suffix);}
#gameContainer { /* size of each ball, represents the size for clickable elements */ --ball-size: 30px; width: 210px; /* make the boundary of the container easier to identify */ border: 2px solid red; /* desirable to have the game not take more than majority of height of screen */ max-height: 90vh; /* plesent spacing between tubes */ display: flex; flex-flow: row wrap; gap: 5px; justify-content: space-evenly; /* when tubes of different heights are on same row align to the bottom */ align-items: flex-end;}.Tube { display: flex; /* order contents from bottom up */ flex-flow: column-reverse nowrap; /* tube is one ball wide and `capacity` balls high */ width: var(--ball-size); height: calc(var(--ball-size) * var(--capacity)); /* if the container size is small enough shrink elements to be a fifth or so of the container, because of gaps and borders this generally means there will never be less than 3-4 tubes per row */ max-width: 20%; /* to show border of tubes */ border: 3px solid blue;}.Ball { /* since the hight of of tube is ball-size*capacity this ends up being just ball-size, but is dynamic so if tube had to be shrunk it stills is the right proportion */ flex-basis: calc(100%/var(--capacity)); align-content: center; text-align: center;}.Ball.A { background-color: rgba(255,0,0,0.5);}.Ball.B { background-color: rgba(0,0,255,0.5);}.Ball.C { background-color: rgba(0,255,0,0.5);}
<body><header><label>--ball-size = <input type="number" value="30" name="--ball-size" onchange="updateGameContainerStyle(this, 'px');">px</label> <br><label>width = <input type="number" value="210" name="width" onchange="updateGameContainerStyle(this, 'px');">px</label> <br></header><main id="gameContainer"><div class="Tube" style="--capacity:4"><div class="Ball A"> A </div><div class="Ball B"> B </div><div class="Ball A"> A </div></div><div class="Tube" style="--capacity:4"><div class="Ball A"> A </div><div class="Ball B"> B </div><div class="Ball C"> C </div><div class="Ball C"> C </div></div><div class="Tube" style="--capacity:4"><div class="Ball B"> B </div><div class="Ball B"> B </div></div><div class="Tube" style="--capacity:3"><div class="Ball C"> C </div><div class="Ball A"> A </div></div><div class="Tube" style="--capacity:3"><div class="Ball C"> C </div></div></main><footer> this is footer content</footer></body>
I would like to effectively specify the .Tube
's height
property as a flex-basis
in the column direction and also flex-shrink: 1
, this means it will shrink the contents to fit within my maximum allowable height. However this is obviously not possible at the same time as using flex-flow: row wrap
.
single column example
To demonstrate my desirable behaviour I've included a near identical copy of the above snippet with only the .Tube {height}
and #gameContainer {flex-flow}
properties altered and fewer tubes to make it more reasonable to see. If you change the ball-size you will see for lower values the size of the whole board is variable but at a certain size it stops getting taller so the whole container still fits within 90vh
and the footer is never overlapped.
(also note the max-width: 20%
is still in effect here which is why the tubes won't grow to be wider than a fifth of the container)
function updateGameContainerStyle2(inputForm, suffix=""){document.getElementById("gameContainer2").style.setProperty(inputForm.name, inputForm.value + suffix);}
#gameContainer2 { /* size of each ball, represents the size for clickable elements */ --ball-size: 30px; width: 210px; /* make the boundary of the container easier to identify */ border: 2px solid red; /* desirable to have the game not take more than majority of height of screen */ max-height: 90vh; /* plesent spacing between tubes */ display: flex; flex-flow: column nowrap; gap: 5px; justify-content: space-evenly; /* when tubes of different heights are on same row align to the bottom */ align-items: flex-end;}.Tube { display: flex; /* order contents from bottom up */ flex-flow: column-reverse nowrap; /* tube is one ball wide and `capacity` balls high */ width: var(--ball-size); flex-basis: calc(var(--ball-size) * var(--capacity)); flex-shrink: 1; /* if the container size is small enough don't shrink elements to be thinner than a fifth or so of the container because of gaps and borders this generally means there will never be less than 3-4 tubes per row */ max-width: 20%; /* to show border of tubes */ border: 3px solid blue;}.Ball { /* since the hight of of tube is ball-size*capacity this ends up being just ball-size, but is dynamic so if tube had to be shrunk it stills is the right proportion */ flex-basis: calc(100%/var(--capacity)); align-content: center; text-align: center;}.Ball.A { background-color: rgba(255,0,0,0.5);}.Ball.B { background-color: rgba(0,0,255,0.5);}.Ball.C { background-color: rgba(0,255,0,0.5);}
<body><header><label>--ball-size = <input type="number" value="30" name="--ball-size" onchange="updateGameContainerStyle2(this, 'px');">px</label> <br><label>width = <input type="number" value="210" name="width" onchange="updateGameContainerStyle2(this, 'px');">px</label> <br></header><main id="gameContainer2"><div class="Tube" style="--capacity:4"><div class="Ball A"> A </div><div class="Ball B"> B </div><div class="Ball C"> C </div></div><div class="Tube" style="--capacity:2"><div class="Ball A"> A </div><div class="Ball B"> B </div></div><div class="Tube" style="--capacity:3"><div class="Ball B"> B </div><div class="Ball C"> C </div></div></main><footer> this is footer content</footer></body>
If I changed the structure of the HTML to have intermediate <div class="row">
I could get the gameboard to be column nowrap
and the rows to be flex-shrink:1; flex-flow: row nowrap
which would meet the sizing criteria but would not redistribute elements amongst rows when the container resizes.
Is there a way to both have a row wrap
like behaviour and shrink
behaviour in the column direction? I suspect there may be a solution using grid layout but the closest I could get was using:
#gameContainer { display: grid; grid-template-columns: repeat(auto-fill, clamp(1ch, var(--ball-size), 20vw));}
which does give basically the same behaviour as the first version, it creates an automatic number of columns that use the ball-size
variable as the width but clamped at being no wider than 20vw (and at least 1 character wide). However I am unaware of any equivalent feature to flex-shrink
using grid display.