Sitemap

Implementation: Resizable Grid

The grid of the web

6 min readJul 5, 2025

Grid layout has been discussed for inclusion in the CSS standard since 2007, but started to be popular only a decade later, when all major browsers finally supported it unprefixed.

The idea wasn’t new (GridBagLayout was shipped in the very first version of Java, a decade before that) but powerful: as soon as you have a grid structure, you can define panes as a combinaison of cells group, spanning horizontally or vertically. But, beyond what the flex layout can do, it is not an exclusive “or”: panes can span on cells in both dimensions at the same time.

Use case

Let’s describe a typical use case with:

  • a header with a fixed height, that will contain switches to show or hide the side panels;
  • a contents area;
  • a left pane to, say, display “settings”;
  • a right pane to, say, display “comments”.
The 2 rows x 3 columns grid structure

In CSS terms, this grid has two rows:

  • one full-width row, spanning 3 columns for a header pane, and 48px high;
  • another row with the sequence of settings, contents and comments cells occupying the remaining available vertical space (1fr). Horizontally, the first and last cells should occupy only the width required by their contents (min-content), the contents one occupying the remaining space:
#parent-pane {
display: grid;
grid-template:
"header header header" 48px
"settings contents comments" 1fr
/ "min-content" 1fr "min-content";
}
}

Then all you have to do is to assign those grid area names to your HTML pane elements:

#settings {
grid-area: settings;
min-width: min-content;
}
#contents {
grid-area: contents;
}
#comments {
grid-area: comments;
min-width: min-content;
}

Quite easy, isn’t it? Now, let’s say that we want to make the panes resizable.

Resizing

The usual UI for resizing is a gutter between resizable panels which, when hovered, displays an horizontal or vertical resizing icon.

The cool but non-standard resize UI 🤷

Unfortunately the implied UX is not natively supported by browsers, and to implement it you need to add JavaScript code to manually listen to pointer events (down, moving and up) then programmatically set new sizes.

Alternatively, the standard way to resize a block does not any require JS, but one a line of CSS:

#settings, #comments {
resize: horizontal; // Or vertical, or both
}

When doing so, a new icon appear at the bottom-right corner of the block. It will be the same, whatever the resize axis your specified:

The standard resize icon, at bottom-right corner

Another side benefit of using it is that it will automatically remembers it new size once you modify it. Also, as a CSS feature, it implicitly propagates that size change to the parent layout. In the case of our grid example, it means that a min-content or a 1fr can be implicitly changed to some greater of smaller pixels size, when the resizing occurs.

Resizing backwards

Now appears an issue we may not have foreseen: if we can only resize from the bottom-right, how can we resize the left position of this pane on the right side of the screen?

The trick here is to reverse the direction of the horizontal flow: if the contents is assumed as flowing from right-to-left (like in arabic or hebrew languages), the resize UI will consistently display at the end of the flow, which becomes at left.

#comments {  
direction: rtl; // right-to-left
}

Ok cool, now the resize UI appears on the left, so that we now can resize toward that direction… but all the block content (text, etc) now displays this way 😱.

To fix this, let’s distinguish the pane container from its contents:

#comments {  
direction: rtl;

.comments-contents {
direction: ltr; // left-to-right
}
}

Now we have both of the benefits.

Showing and hiding

The goal here is to change the default widths of the panes (min-content) with zero (to collapse them), depending on the switch state. To do so, let’s change them from constant to variables:

#parent-pane {
--settings-width: min-content;
--comments-width: min-content;

grid-template:
"header header header" 48px
"settings contents comments" 1fr
/ var(--settings-width) 1fr var(--comments-width);
}
}

Then, let’s add a bit of JavaScript to handle the switches clicks:

const settingsCheck = document.querySelector("#settings-check");
const commentsCheck = document.querySelector("#comments-check");

const parentPane = document.querySelector("#parent-pane");
const settingsPane = document.querySelector("#settings");
const commentsPane = document.querySelector("#comments");

settingsCheck.onchange = (e) => {
parentPane.style.setProperty(
"--settings-width",
settingsCheck.checked ? "min-content" : "0"
);
};

commentsCheck.onchange = (e) => {
parentPane.style.setProperty(
"--comments-width",
commentsCheck.checked ? "min-content" : "0"
);
};

Here is an example of the result:

Resizable panes

Now in this example, the switch is a bit too «flip/flop»: we would like to have a smoother transition, in which the side panes collapse and expand progressively when hidden or shown. How can we do that?

Transitions

Such an improvement becomes tricky because transitions only work between discrete values. That means that we should set the exact widths of the columns: either 0 (to hide them) or any pixel value set by the user when resizing. Of course, we still want to values to never be below each panel’s min-content.

To do so, we have to remember two things:

First, we should not try to set the width of the #settings, #contents and #comments elements themselves, because what we want to control is the whole layout, not the individual elements (actually we never any size on the elements themselves). So the transition should apply on the grid columns:

#parent-pane {
transition: grid-template-columns 0.3s;
}

Second, since we already created widths variables (--settings-width and --comments-width CSS properties), all we have to do is to use their values instead of the min-content “constant”:

let settingsWidth;
let commentsWidth;

settingsCheck.onchange = (e) => {
parentPane.style.setProperty(
"--settings-width",
settingsCheck.checked ? settingsWidth + "px" : "0"
);
};

commentsCheck.onchange = (e) => {
parentPane.style.setProperty(
"--comments-width",
commentsCheck.checked ? commentsWidth + "px" : "0"
);
};

But where do the values of those settingsWidth and commentsWidth JS variables come from? We read them by observing the resizing of the panes:

const settingsObserver = new ResizeObserver((entries) => {
settingsWidth = entries[0].borderBoxSize[0].inlineSize;
parentPane.style.setProperty("--settings-width", settingsWidth + "px");
});
settingsObserver.observe(settingsPane);

const commentsObserver = new ResizeObserver((entries) => {
commentsWidth = entries[0].borderBoxSize[0].inlineSize;
parentPane.style.setProperty("--comments-width", commentsWidth + "px");
});
commentsObserver.observe(commentsPane);

Now that should work… aside some glitches: because reading those values and setting them on the CSS variables triggers unwanted immediate UI updates. To make sure to have transitions visually occur only when hitting the switches, let’s disable the transition in the interim:

settingsCheck.onchange = (e) => {
parentPane.style.setProperty("--transition", "0.3s");
parentPane.style.setProperty(
"--settings-width",
commentsCheck.checked ? settingsWidth + "px" : "0"
);
};
commentsCheck.onchange = (e) => {
parentPane.style.setProperty("--transition", "0.3s");
parentPane.style.setProperty(
"--comments-width",
commentsCheck.checked ? commentsWidth + "px" : "0"
);
};
const settingsObserver = new ResizeObserver((entries) => {
settingsWidth = entries[0].borderBoxSize[0].inlineSize;
parentPane.style.setProperty("--transition", "0");
parentPane.style.setProperty("--settings-width", settingsWidth + "px");
});
const commentsObserver = new ResizeObserver((entries) => {
commentsWidth = entries[0].borderBoxSize[0].inlineSize;
parentPane.style.setProperty("--transition", "0");
parentPane.style.setProperty("--comments-width", commentsWidth + "px");
});

Now everything is working fine. Here is an example of doing it:

Toggling panes with transitions

--

--

Jérôme Beau
Jérôme Beau

Written by Jérôme Beau

Sharing learnings from three decades of software development. https://javarome.com

No responses yet