White whale layout

Mar 1, 2026, 11:31 PM
Δ
Mar 7, 2026, 10:05 PM
SiteMeta CSS WebDesign HTML Blog

If you’re looking at this site sometime in early/mid-2026, you’re looking at the fruits of a year on (and off) the Cascading Style Seas in dogged pursuit of a single quarry: this layout. I finally got the stupid freaking layout to work. And it’s so, so, dumb. And I like it so much that I keep pulling up my website to look at it.

It’s everything I dreamed, and also fundamentally unsatisfying and transitory. Like every redesign I’ve ever done. 🙂

The (hazy) vision

If you poke through all my posts, from the first inklings of last year’s resuscitation up through links I’ve been adding this week, an indelible vibe coalesces: something in the space between Inter Sans, “Cassette Futurism”, mid-century corporate, WipEout (and now Marathon, I guess?), and the goosebumps I get every time a location title card pops up in Control.

What does that translate to in practice? In my pea brain it’s simple: HUGE TITLE TEXT.

How do you get HUGE TITLE TEXT? Less simple! (For me, at least…)

Ideally, I wanted my title text to take up the left ~50% of the screen, and the content of the post to fill in the right half. But, I also wanted it the layout to switch to a vertical stack when the viewport width is small enough (because 50% of your screen being HUGE TITLE TEXT is not so fun anymore on a portrait-oriented phone screen). That should be easy! Sounds like it should just be a The Sidebar, as the pros say.

The catch

The catch was that I sometimes can’t help myself from being a semantic ideologue.

The thing about a The Sidebar is that it works on a container with two children elements. The natural approach here would be to let my heading be one of the elements, and then wrap the rest of the post in a div to get my to children. But a div is not semantic, per se, and I’ve been trying to be real good about writing semantic HTML—to the point that I couldn’t bring myself to add a little wrapper and just make the thing work.

(I actually did try adding the wrapper, briefly, but hit the limits of my flexbox understanding before I could get any decent results.)

Failing that endeavor, I resigned myself to a layout that was a single column with a large header followed by the body of the post. (It’s actually functionally identical to the “updated” layout if your screen isn’t very wide.) My gripe with this layout was that it left huge amounts of negative space on wide displays, while limiting the maximum size of the title text. The sole little column just felt sad and lonely and empty, alas.

The catch

With my recent data-rejiggering finished up, I found myself jumping around a bunch of my pages, and also re-playing Control, and seeing gorgeous giant text that my sad little solo column could never do justice to. So I decided it was time to roll up my sleeves and force myself to learn some actual principled CSS.

Into the grid

I tried my hand at flexbox tricks again, but realized it was just fundamentally not what I was looking for. In MDN’s own words:

[Flexbox] is a one-dimensional layout model for distributing space between items

To cheat flexbox into the shape I wanted, I’d have to set the flex direction to column, which intuitively felt icky. This explained why: I didn’t want a 1-dimensional layout that spaced out columns—I actually wanted a 2-dimensional layout that distributed particular elements in particular ways.

Luckily, that’s what CSS grids are for! I just needed to figure out what rules would present things how I wanted.

The goal for the layout is this:

Definitions

Here’s what I ended up with (lightly editorialized):

article {
  /* ... */
  
  /*  some vars for the columns */
  --col-size: var(--measure);
  --n-cols: 2;
  --gap: var(--s1);
  
  /* Spacing between grid elements */
  column-gap: var(--gap);
  row-gap: 0;
  
  /* Grid logic! */
  display: grid;
  grid-template-columns:
    repeat(
      auto-fill,
      minmax(0, var(--col-size))
    )
  ;
  justify-content: center;
  /* set the max width to col-size * n-cols to get the right col number. */
  max-inline-size: calc(
    var(--gap) + var(--col-size) * var(--n-cols)
  );
  
  /* Element spacings */
  & > * {
    grid-column: auto / -1;
    /* Block margins need to go here, because
    we need to keep `row-gap` to 0 or alignment is ruined */
    margin-block: calc(var(--gap) / 2);
  }
  
  /* Special casing for header/h1, so they can be huge! */
  & > header, h1 {
    grid-column: 1 / -2;
    grid-row: span 16;
    align-self: start;
    margin-block: 0;
  }
  /* ... */
}

The rules for my grid are as follows:

This all worked great, and was pretty much everything I hoped for!

Except… (fake masonry)

Except, the thing with grids is that they’re grids: columns and rows are fixed sizes. If one grid square’s 50ft tall, its neighbor also has to be 50ft tall. This is a problem for my silly little layout, where I’m intentionally making the top cell HUGE (like, sometimes twice the view height) and the content next to it might be literally one 1em-size sentence. Which means, if I don’t do something, the whole layout is kinda actually total trash.

Having rows that are different sizes is actually the whole point of the CSS Grid’s whole Masonry thing, but that’s not the future we’re living in yet.

In the meantime, I found a workaround for my very specific situation: the header’s grid-row: span 16; expression. This makes the header stretch up to 16 “rows,” which is hopefully more than we’ll ever need to avoid awkward gaps. (It would make more sense to just do a “stretch all rows,” but you actually can’t do that with “implicit rows,” which is what we have here. I couldn’t figure out how to turn my rows into explicit rows, so just picking a big-ish number seems like the next best option.)

There’s one last wrinkle in this strategy, which is the fact that spanning implicit rows still incurs bonus row gaps—so if the row gap is 2em, we’ve got a 32em space crammed after our header. This ends up super wonky if the body text is short and we’re in a widescreen mode, but is also immediately a huge issue in single-column mode, because there’s a big ugly meaningless gap fixed between the header and start of the post.

To resolve the space jank, I ended up setting the row gap to 0 and instead handling spacing for elements more manually by setting margins (i.e. the margin-block default set on grid elements).

It’s not quite as clean as I’d like, but it gets the job done, and hopefully we’ll eventually live in a future where this is just a thing you can do easily.

And some other things

While I was messing around, I made some other tweak that I ended up liking:

I hope one day I figure out a more elegant way to make the last one happen, but in the meantime I’ve settled for this approach (pilfered from Phil Archer) to get those header numbers working:

article {
  /* ... */
  /* Counters for headers! */
  counter-reset: h2;
  & :is(h1, h2, h3, h4, h5, h6)::after{
    float: inline-end;
    height: 100%;
    font-weight: 100;
  }
  & h2 {
    counter-reset: h3;
    &::after {
      counter-increment: h2;
      content: counter(h2);
    }
  }
  & h3 {
    counter-reset: h4;
    &::after {
      counter-increment: h3;
      content: counter(h2) "." counter(h3);
    }
  }
  & h4 {
    counter-reset: h5;
    &::after {
      counter-increment: h4;
      content: counter(h2) "." counter(h3) "." counter(h4);
    }
  }
  & h5 {
    counter-reset: h6;
    &::after {
      counter-increment: h5;
      content: counter(h2) "." counter(h3) "." counter(h4) "." counter(h5);
    }
  }
  & h6 {
    &::after {
      counter-increment: h6;
      content: counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6);
    }
  }
}

I’m liking it quite a bit—we’ll see how long that last!