« back to posts

Sizing a container to the wider of two states

2024-06-29 · view article source

This post presents a simple CSS recipe to keep layouts consistent when an element can transition between two states with differently sized contents. It bears some similarity to the behavior of the CardLayout layout manager in Java Swing: you want the component to be large enough to support any of its children, but only show one of them at once.

The sizing problem

Suppose that you have a button that can be in one of two states: say, pressed or not pressed. The contents of the button differ between the two states:

Since the contents in the two states may have different widths, a naïve implementation of this button will change widths when its state changes. This is often undesirable, since we don’t want other components to reflow around the button. We also want the click target area to be consistent: a given point should be in the click area either always or never.

One simple approach to making the button the same width in the two states is to just pick a fixed width and use it: say, 100px:

Wait, that’s too narrow. 200px?

This approach kind of works, but it’s a bit finicky. You need to manually find a width that works well for the element, and the padding probably won’t be quite consistent with your application’s usual styles. You’ll need to update the width if you change the contents, and if the content can be dynamic (including appearing in more than one language) then you’re right out of luck.

A smashing solution

Here’s an approach that manages to use the natural element widths without any manual calculations:

The idea is to always render both states in a column layout, one above the other, but let the inactive state have zero height so that it only contributes to its parent’s width:

.two-state-container {
    display: flex;
    flex-direction: column;
    align-items: center;
}
.inactive-state {
    height: 0;
    visibility: hidden;
}
<!-- e.g., in the "pressed" state: -->
<button class="two-state-container">
    <span class="inactive-state">Copy to clipboard</span>
    <span>Copied!</span>
</button>

I think of this as “smashing” the inactive state, since it’s similar to what the built-in macro \smash does in TeX\TeX.

This approach is not perfect. It only works in the horizontal direction, so if one of the states is wide enough that it wraps onto multiple lines, then the container will still need to reflow when states change, which breaks the illusion:

This is a narrow container: the buttons are as big as they can get!

You can imagine trying to apply a similar trick with a horizontal flex container, but setting an element to width: 0 will change its wrapping and thus its height! Likewise, this won’t work if the contents’ width depend on their height: for instance, an image with a fixed aspect-ratio: and height: 100%. But although it’s imperfect, it is easy to understand and it works well in a decent range of cases, so I’m happy to have it in my toolbelt.

« back to posts