Skip to content

Instantly share code, notes, and snippets.

@thysultan
Forked from Heydon/algorithmic-layouts.md
Created November 13, 2021 17:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thysultan/e2732127301092abf5588540ff7367d5 to your computer and use it in GitHub Desktop.
Save thysultan/e2732127301092abf5588540ff7367d5 to your computer and use it in GitHub Desktop.

Algorithmic layouts

You are looking at the most important, and most abundant thing on the web. You can't see it, unfortunately, because it's very small… aaaaand it's invisible — so having a magnifying glass doesn't really help here. But still.

I'm talking, of course, about U+0020; not to be confused with the band U2, who are just as ubiquitous, but far less useful.

This unicode point, representing the humble space character, is between every word, in every run of text, on every page of the web. And it has a very special characteristic: it's not sticky like glue. If two words are neighbors but there's not enough room for both of them, the space will free the second word to wrap around and start a new line.

Before getting into flexible containers, viewport meta tags, and @media breakpoints this humble character is what makes the web fundamentally 'responsive'. That is: able to change the layout of its content to suit different devices, contexts, and settings. Browser text does this automatically because it's kind of self-aware, just not self-aware in the scary Terminator/Skynet way.

Responsive content can be expressed mathematically. To find the height of an element, you divide the area by the width. To find the width, you divide the area by the height. Given a <div> that is 50rem wide and 25rem high, if I reduce the width to 25rem, the height will have to grow to 50rem to still contain the same amount of text.

height == area / width
width == area / height

In responsive design, we capitalize on the text wrapping algorithm by refusing to hard code widths and heights — letting our elements, and the text and media they contain, fill the available space. The vastly inferior alternative is to take inventory of all our users' device types and settings, tailoring specific, fixed designs to each. This is sometimes known as 'adaptive' design, or simply: b******s.

But even with flexible layouts, we still sometimes have to intervene and 'manually override' the layout process using @media breakpoints.

For example, a sidebar can only be a sidebar if there's room for one. Where there's no room for two adjacent elements, they must be placed one atop the other. Employing a breakpoint at, well, the point of breakage, is the standard solution.

@media (max-width: 20rem) {
  .sidebar, .not-sidebar {
    width: 100%;
    float: none;
  }
}

But the CSS Flexbox module lets us solve this problem algorithmically instead — without the need for breakpoints.

What makes a sidebar? Visually, an adjacent element that takes up less horizontal space than its neighbor. Therefore, the rule (or 'axiom') that underlies the layout is that the sidebar's companion should never be less than 50% in width:

.not-sidebar {
  min-width: 50%;
}

As for the sidebar itself, we give that a flex-basis of something like 20rem. Flex basis represents the 'ideal' width, and does not preclude the element from growing or shrinking under suitable circumstances.

.not-sidebar {
  min-width: 50%;
}

.sidebar {
  flex-basis: 20rem;
}

If I give the container element flex-wrap: wrap, the sidebar's companion will be forced onto a new line as soon as the 50% threshold is reached. It works just like the text wrapping algorithm but with HTML elements rather than words.

.with-sidebar { /* the common parent element */
  display: flex;
  flex-wrap: wrap;
}

.not-sidebar {
  min-width: 50%;
}

.sidebar {
  flex-basis: 20rem;
}

Now we just need to ensure each element, the sidebar and the non-sidebar, grows to take up the available space. The sidebar takes flex-grow: 1 (which essentially means 'yes please: grow if there's free space') and the non-sidebar takes an absurdly high flex: 666 (most people choose 999 but I'm a satanist).

.with-sidebar { /* the common parent element */
  display: flex;
  flex-wrap: wrap;
}

.sidebar {
  flex-basis: 20rem;
  flex-grow: 1;
}

.not-sidebar {
  min-width: 50%;
  flex-basis: 0;
  flex-grow: 666;
}

The reason for this high value is to ensure the non-sidebar takes up all of the free space wherever the two elements are adjacent. The sidebar's flex-basis value is not counted as free space and is subtracted from that calculation.

Here's something that's nice: that min-width: 50% declaration? It refers to the parent element's width, not the viewport width. This makes the behavior more like that of a so-called 'container query' than a traditional media query. Container queries are still being specified and so far haven't been adopted by browsers. Here, we've kind of mimicked the behavior of a container query without using anything like a JavaScript polyfill.

What if we want a margin between our sidebar and non-sidebar? If we add a margin to the sidebar's right-hand side, we solve the problem for contexts where the two elements are adjacent. Things get weird and gross when the non-sidebar wraps...

The smart solution adds margin all around the two elements then—and this part id important—uses a negative margin on a common parent element to take the excess margin away. It's kind of like removing the excess pastry around a pie lid.

Now, margin will appear between the two elements in either configuration, but the sides will always be flush, and there will be no extra margin above or below the component.

If you want a gap of 1rem, you need to add a margin of 0.5rem to each of the elements, then remove 0.5rem from the parent.

.with-sidebar {
  margin: -0.5rem;
}

.with-sidebar > * {
  margin: 0.5rem;
}

The upshot is the component can take a sidebar of whatever width you like, and Flexbox will take care of the layout no matter the context. This <with-sidebar> custom element just needs the width, margin, and side properties, and any two child elements.

<with-sidebar width="15rem" margin="1rem" side="right">
  <div>
    <h2>Sidebar</h2> 
    ... 
  </div>
  <div>
    <h2>Not a sidebar</h2> 
    ... 
  </div>
</with-sidebar>

Inside the component, we can add the not-sidebar class to the appropriate child element, based on the value of the side prop. Since this.children is an array, 0 represents the first, or left, child and 1 the second or right one.

this.side = this.getAttribute('side');
const notSide = this.side === 'left' ? 1 : 0;
this.children[notSide].classList.add('not-sidebar');

YEAH BOIIIII.

So what if we wanted to create an effortlessly responsive grid? The algorithms powering the CSS Grid module make this similarly easy. In Grid's case, all of the necessary properties are placed on the parent.

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  grid-gap: 1rem;
}

The grid-template-columns property sets out the grid into which child elements are positioned. The repeat() function means grid cells are created on-demand to accommodate any number of child elements. The auto-fill keyword tells cells to distribute themselves to take up the available space, wrapping where necessary, and minmax sets out the minimum and maximum width for any and every cell.

Grid gap adds gaps only between cells. It has the same effect as the negative margin technique discussed for the Flexbox sidebar component, but isn't a hack.

The resulting behavior is one of flexibility, but still order. No breakpoint interventions are necessary at any point along the spectrum of all viewport widths. It just works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment