<rss version="2.0">
  <channel>
    <title>QuirksBlog</title>
    <link>https://quirksmode.org/quirksblog/</link>
    <description>ppk's blog since 2003</description>
    <item>
      <title>Grid algorithm adventures</title>
      <link>https://quirksmode.org/quirksblog/2026/0527-gridalgo.html</link>
      <pubDate>2026-05-27T12:00:00+02:00</pubDate>
      <description><![CDATA[<style>
.nifty > :nth-child(4n) {
	grid-column: 4;
}

.nifty-row > :nth-child(4n) {
	grid-row: 4;
}
</style>

<link rel="stylesheet" href="/quirksblog/column-fill.css">
<script src="/quirksblog/column-fill.js"></script>

<p>For my <a href="0505-cfg1.html">recent</a> <a href="0507-cfg2.html">series</a> about column-filled grids I wrote an overview of parts of the grid algorithm. I ended up not using it in that series, but thought I&#8217;d publish it anyway.</p>

<p>During editing I got caught up in the spirit of things and explained why, as so often, it&#8217;s better to hard-code a relatively small number of constraints and let browsers handle the rest.</p>

<p>So here it goes; a simplified summary of the Grid algorithm for slightly-advanced CSS developers with lengthy detours and no real point.</p>

<h3>Implicit tracks</h3>

<p>As you know, a grid consists of columns and rows. Together, they are called <strong>tracks</strong>, and in some respects they work the same.</p>

<p>Tracks can be <em>explicit</em>, which means a property defines them. This snippet creates two explicit column tracks.</p>

<pre>
grid-template-columns: 1fr 1fr;
</pre>

<div class="columnGrid narrow styled" data-items="6"></div>

<p>This grid has two columns and six items. We didn&#8217;t define any rows. How many rows does it have?</p>

<p>Well, duh. Three.</p>

<p>A <em>duh</em> is nearly always a sign of <em>implicit tracks</em>. The grid creates three rows because the item placement of the six items requires them. They&#8217;re here because they&#8217;re needed. You didn&#8217;t define them, but they&#8217;re implicit in the entire grid structure.</p>

<h3>grid-row and grid-column</h3>

<p>I hope the existence of <code>grid-row</code> and <code>grid-column</code> does not come as a surprise to you. They allow you to explicitly place an item in a specific row and/or column. My <a href="0505-cfg1.html">technique</a> rests on explicitly assigning rows to items.</p>

<p><a href="https://css-articles.com/" class="external">Temami</a> pointed out something I didn&#8217;t know, or hadn&#8217;t fully realised. What does the following code do?</p>

<pre>
.grid {
	grid-template-columns: 1fr 1fr; 
}

.grid > :nth-child(4n) {
	grid-column: 4;
}
</pre>

<p>Well, it places the 4th, 8th, etc. item in the fourth column. Duh. Makes sense, doesn&#8217;t it? You obviously want a four-column layout.</p>

<div class="columnGrid narrow nifty" style="grid-template-columns: 1fr 1fr" data-title="Explicit and implicit columns" data-items="9"></div>

<p><em>Duh</em> again signals implicit tracks. The <code>grid-column: 4</code>  creates a fourth column, and the existence of a fourth column implies the existence of a third column. So the grid has a third column.</p>

<p class="smaller">Fun challenge for semantic nerds: is the fourth column explicitly or implicitly defined? Discuss.</p>

<h3>grid-auto-columns and -rows</h3>

<p>But why are the third and fourth column so narrow? That&#8217;s because they don&#8217;t have a defined size. The explicitly defined first and second columns do: <code>1fr</code>. But the implicit third and fourth column have the default width of <code>auto</code>. In Grid, that&#8217;s a complicated value, but a first approximation is <code>min-content</code>: as little as we can get away with given the content.</p>

<p>So the third and fourth column get their minimally necessary width, and the first and second divide the rest of the width among them.</p>


<pre>
.grid {
	grid-template-columns: 1fr 1fr; 
	<strong>grid-auto-columns: 1fr;</strong>
}
</pre>

<div class="columnGrid narrow nifty" style="grid-template-columns: 1fr 1fr; grid-auto-columns: 1fr" data-title="Now with grid-auto-columns: 1fr" data-items="9"></div>

<p><code>grid-auto-columns</code> and <code>-rows</code> set a width and height for automatically-created columns and rows. Adding <code>grid-auto-columns: 1fr</code> solves the issue: now all columns have width <code>1fr</code> and the end result is much better.</p>

<p>For rows, <code>auto</code> means "the minimum height necessary", and that&#8217;s usually the perfect height for grid rows. That&#8217;s why we generally don&#8217;t bother setting <code>grid-auto-row</code>.</p>

<h3>grid-auto-flow</h3>

<p>Why does the ninth item in the example below create a third row? Why not a fifth column? What makes it prefer rows over columns?</p>

<p><code>grid-auto-flow</code> does. It can be <code>row</code> (the default) or <code>column</code> and means something like "if you&#8217;re forced to create a new implicit track, make it a row/column." Since in our example it&#8217;s <code>row</code> we get a third row, and not a fifth column.</p>

<div class="columnGrid narrow" style="grid-template-columns: auto" data-items="3" data-title="grid-auto-flow: row; no columns defined"></div>

<p>You&#8217;ve probably seen a grid like this a few times when you made a syntax error in your column definition. 
If you don&#8217;t define any columns, each item creates its own row because that&#8217;s what <code>grid-auto-flow: row</code> says it should do.</p>

<pre>
.grid {
	grid-auto-columns: 1fr;
	grid-auto-flow: column;
}
</pre>

<div class="columnGrid narrow" style="grid-auto-flow: column; grid-template-columns: 1fr; grid-auto-columns: 1fr" data-title="grid-auto-flow: column; no rows defined" data-items="9"></div>

<p>The other, more rarely used value is <code>column</code>. Now the grid prefers to create columns for extra grid items. This simple example creates a new column for every grid item.</p>

<h4>Column flow</h4>

<p>Let&#8217;s expand this column flow example. We use the same code as before to place the 4th, 8th etc. item in the fourth column.</p>

<pre>
.grid {
	grid-auto-columns: 1fr;
	grid-auto-flow: column;
}

.grid > :nth-child(4n) {
	grid-column: 4;
}
</pre>

<div class="columnGrid narrow nifty" style="grid-auto-flow: column; grid-template-columns: 1fr; grid-auto-columns: 1fr" data-title="flow: column; nth-child(4n) grid-column: 4" data-items="9"></div>

<p>A lot starts happening at once, and some of it is familiar. The fourth and eighth items are placed in column four. Like before, this creates four implicit columns, and the ninth item adds a fifth column because <code>grid-auto-flow: column</code> tells the grid to create extra columns when necessary; not rows.</p>

<p>However, the <code>grid-column</code> does something else as well. When placing the eighth item in the fourth column, it can&#8217;t go in the first row because the fourth item is already in row 1 / column 4. So we obviously need a second row and it&#8217;s implicitly created.</p>

<p class="smaller">But what&#8217;s with the weird item order? Well, first we count top-to-bottom in the leftmost column, and we already saw why the example has two rows. So the placement of items 1, 2, and 3 is understandable. 4 goes in column four, even though this is actually the seventh cell in logical order. We commanded this explicitly, after all. So 5 goes in the fourth cell, 6 in the fifth, 7 in the sixth, 8 is again forced into the fourth column but happens to take up the eighth cell. 9, finally, has to create a new track, and <code>grid-auto-flow: column</code> makes that track a column. We gave explicit instructions, and they&#8217;re obeyed to the letter, even when they don&#8217;t really make sense.</p>


<h3>Grid item placement algorithm</h3>

<p>Let&#8217;s go to the formal algorithm.</p>

<p>First, the grid calculates the minimum necessary number of columns and rows. This information comes from explicit sources such as  <code>grid-template-*</code>, but also from declarations such as the <code>grid-column: 4</code> we saw above.</p>

<p>The grid also determines from <code>grid-auto-flow</code> what kind of tracks to create for 'overflow' items: rows or columns. Let&#8217;s say that it&#8217;s rows; that use case is more popular and more familiar to everyone.</p>

<ol>
	<li>Items with both a defined <code>grid-column</code> and a <code>grid-row</code> are placed in their correct column and row, creating implicit tracks if necessary.</li>
	<li>Next, items with a defined <code>grid-row</code> are placed in the leftmost empty position in their correct row, creating implicit tracks if necessary.</li>
	<li>Then, all remaining items, including those with a defined <code>grid-column</code>, are placed, in <code>order</code> order, with source code order breaking any ties. However, any item with <code>display: none</code> is skipped.
	<ol style="list-style-type: lower-latin">
		<li>A 'cursor' is created that points to the leftmost empty cell in the first row.</li>
		<li>The first unplaced item is placed there. However, if that item has a <code>grid-column</code>, the pointer moves to the next empty cell in that column and places the item there. This step may move the cursor to the next row.</li>
		<li>Then the cursor moves to the next cell in the row, or the leftmost empty cell in the next row, creating new implicit rows if necessary.
		<li>During this entire process the cursor only moves forward, never back. Thus, there may be empty spots in the grid.</li>
	</ol>
</ol>

<p>If <code>grid-auto-flow</code> is <code>column</code>, all instances of 'row' and 'column' in the algorithm above are swapped, and 'leftmost' is replaced by 'topmost'.</p>


<h3>Strict grid instructions</h3>

<p>I&#8217;d like to focus on step 2.</p>

<blockquote>
<p>Items with a defined <code>grid-row</code> are placed in the leftmost empty position in their correct row, creating implicit tracks if necessary.</p>
</blockquote>

<p>Earlier, I created a <a href="/quirksblog/2026/0505-cfg1.html">technique</a> for  a filling a grid column by column (as if <code>grid-auto-flow</code> is <code>column</code>), but that also sets the maximum number of columns as if it uses a <code>grid-template-columns</code> with <code>auto-fill</code>. It works as follows:</p>

<pre>
.columnGrid {
	display: grid;
	grid-auto-columns: 1fr;
	
	& > * {
		--gridWidth: calc(100cqw - var(--padding) * 2);
		--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
		--rows: round(up,calc(sibling-count() / var(--maxColumns)));
		--row: calc(mod(calc(sibling-index() - 1),var(--rows)) + 1);
		<strong>grid-row: var(--row);</strong>
	}
}
</pre>

<div class="columnGrid column narrow tags" data-items="7" data-title="The technique in action."></div>

<p>It first determines the maximum number of columns, and from that the amount of rows that it needs. Then it assigns the correct <code>grid-row</code> to every grid item.</p>

<p>In other words, it only uses step 2 of the grid algorithm.</p>

<p>And that&#8217;s a problem. It feels to me like using <code>position: absolute</code> all over your site. Yes, it works, but only in perfect circumstances, and as soon as anything goes wrong a <em>lot</em> goes wrong. The technique is brittle.

<p>I <a href="/quirksblog/2026/0507-cfg2.html">saw that and tried</a> to create a better technique, but it needs <code>children-count()</code>, which doesn&#8217;t exist yet.</p>

<h3>Looser instructions</h3>

<p>It would be better if we could use a lighter touch and set only a few items to their correct row, like we did above.

<pre>
.columnGrid {
	display: grid;
	grid-auto-columns: 1fr;
}
	
.grid > :nth-child(4n) {
	grid-row: 4;
}
</pre>

<div class="columnGrid narrow nifty-row" style="grid-auto-flow: column; grid-auto-columns: 1fr" data-title="flow: column; nth-child(4n) grid-row: 4" data-items="7"></div>

<p>Fewer instructions, fewer things that can go wrong.</p>

<p>Remember, the grid now prefers to create imnplicit columns. It&#8217;s implicitly ordered to have four rows, though, so it&#8217;s only the fifth item that starts a new column. </p>

<h4>The hide test</h4>

<div class="columnGrid narrow nifty-row hole" style="grid-auto-flow: column; grid-auto-columns: 1fr" data-items="7" data-title="The second item is hidden"></div>

<p>But this version is not perfect. When we hide one item, the grid doesn&#8217;t flow nicely into the hole. That&#8217;s understandable: we told it to put the fourth item in the fourth row, and it does so. 

<p>Here&#8217;s the relevant portion of the algorithm again, but now with <code>grid-auto-flow: column</code> engaged:</p>

<blockquote>
<ol style="list-style-type: lower-latin">
	<li>A 'cursor' is created that points to the topmost empty cell in the first column.</li>
	<li>The first unplaced item is placed there. However, if that item has a <code>grid-row</code>, the pointer moves to the next empty cell in that row and places the item there. This step may move the cursor to a lower column.</li>
	<li>Then the cursor moves to the next cell in the column, or the topmost empty cell in the next column, creating new implicit columns if necessary.
	<li>During this entire process the cursor only moves forward, never back. Thus, there may be empty spots in the grid.</li>
</ol>
</blockquote>

<div class="columnGrid narrow nifty-row hole" style="grid-auto-flow: column; grid-auto-columns: 1fr" data-items="7" data-title="The second item is hidden"></div>

<p>So the 'cursor' starts in the top left cell, the first item is placed there, and the cursor moves to the next cell down. The second item is skipped because it has <code>display: none</code>. The third item is placed in the second cell. Then the fourth item is placed in row 4, and the third cell is skipped. Since it was calculated there&#8217;s a maximum of four rows, the cursor goes to the next column and places the remaining items there.</p>

<div class="columnGrid column narrow hole" data-items="7" data-title="A hole in the original technique"></div>

<p>This is better than the original technique, which goes wrong even more spectacularly because every item is forced into a row, whether that makes sense or not.</p>

<p>Still, it&#8217;s not perfect. The original technique placed all items in a specific row. The looser technique only places a few items in a row. That is better, but in both cases we don&#8217;t check whether the item placement <code>makes sense</code> (and, to be honest, I&#8217;m not sure that&#8217;s possible in CSS alone).</p>


<h4>Maximum flexibility</h4>

<p>So far we tried to tell items what to do instead of programming a few constraints and then go out of the way and let browsers handle the exact placement. We should just tell browsers how many rows or columns we need, but don&#8217;t handle individual items. The next example does that.</p>

<pre>
.columnGrid {
	display: grid;
	grid-template-rows: repeat(4,1fr); 
	grid-auto-flow: column; 
	grid-auto-columns: 1fr
}
</pre>

<div class="columnGrid narrow hole" style="	grid-template-rows: repeat(4,1fr); grid-auto-flow: column; grid-auto-columns: 1fr" data-items="7" data-title="Four rows hard-coded"></div>

<p>This works best. Four rows are created, and the items fill up these rows neatly. Item 4 is in the third cell, as it should be when item 2 is hidden. Unfortunately, as I <a href="/quirksblog/2026/0507-cfg2.html">explained earlier</a>, I can&#8217;t calculate how many rows my technique needs because of the lack of <code>children-count()</code>. But if you can hard-code the number of rows this is the best technique.</p>

<h3>Conclusion</h3>

<p>As is so often the case in web development, the best way to create a layout is to give as few instructions as possible and then get out of the way and let browsers sweat the details. Grid, like many other CSS modules, was written with a lot of sensible defaults in place. As long as you don&#8217;t overrule those defaults too much, your site will behave well even in adverse circumstances.</p>]]></description>
    </item>
    <item>
      <title>performance.now() ticket sales start</title>
      <link>https://quirksmode.org/quirksblog/2026/0526-perf-start.html</link>
      <pubDate>2026-05-26T12:00:00+02:00</pubDate>
      <description><![CDATA[<style>
td {
  	padding: 8px;
  	vertical-align: top;
	max-width: 15em;
	--color-primary: var(--main);
  	
	tr.soldout & {
		font-size: 80%;
		color: #999;
		
		&:nth-child(1)::after {
			content: 'Sold out';
			display: block;
	  		color: var(--color-primary);
		}
	}

  	&:first-of-type {
  		text-align: right;
  		color: var(--color-primary);
  		font-weight: bold;
  	}

	&:nth-child(2) {
		font-size: 80%;
	}

  	&:nth-child(4) {
  	}

  	
  	&:nth-child(3) {
  		color: var(--color-primary);
  		font-weight: bold;

		tr.soldout & {
			font-weight: normal;
			text-decoration: line-through;		
		}
		
  	}
  	
  }
</style>

<p>Public service announcement: ticket sales for the <a href="https://perfnow.nl/" class="external">performance.now()</a> web performance conference, 19th and 20th of November, Amsterdam, have started.</p>

<p>Join <a href="https://timkadlec.com" class="external">Tim Kadlec</a>,
	<a href="https://www.linkedin.com/in/tammyeverts/" class="external">Tammy Everts</a>,
	<a href="https://csswizardry.com" class="external">Harry Roberts</a>,
	and me for two days of advanced web performance geekery, where all the 
global experts will explain what they&#8217;ve been up to for the past year.</p>

<p>In order to break with our past, and our very topic, this year we&#8217;ll announce speakers nice and <em>slowly</em>. Tim, Tammy, and Harry are always there; no surprise. But which web performance luminaries will join them in November? Watch this space. Or any space in general.</p>

<p><a href="https://perfnow.nl/tickets.html" class="external">Get your tickets now</a>.</p>

<p>Early-bird tickets are meanwhile sold out, but you can still get tickets for the lowest regular price. This year, we raise prices twice. To quote the site:</p>

<table>
		<tr>
			<td>eager</td>
			<td>The affordable setting for the thrill-seeking buyer. 
			Astonish the world with your speed.
			Become a thought leader among your peers. Bask in our admiration.</td>
			<td>€700</td>
			<td>Until sold out or 30th of June</td>
		</tr>
		<tr>
			<td>auto</td>
			<td>The sensible default setting for the solid, dependable buyer. Neither too much nor
			too little. Was that a yawn you saw? Not at all, just a little stretching of the jaw muscle.</td>
			<td>€800</td>
			<td>Until sold out or 30th of September</td>
		</tr>
		<tr>
			<td>lazy</td>
			<td>The careful setting for buyers in delicate corporate environments. 
			We hear you. We appreciate you. We also wish you'd hurry up just the tiniest bit.
			You're stressing us out.</td>
			<td>€900</td>
			<td>From 1st of October	</td>
		</tr>
	</table>
	
	
<p>This public service announcement now ends.</p>]]></description>
    </item>
    <item>
      <title>From the archives: New speakers, devrels, and videos</title>
      <link>https://quirksmode.org/quirksblog/2026/0508-videos.html</link>
      <pubDate>2026-05-08T12:00:00+02:00</pubDate>
      <description><![CDATA[<p><a href="/blog/archives/2018/05/new_speakers_de.html">Exactly eight years ago</a> I wrote this short piece about devrel departments not putting up videos of their proposed speakers. The problem hasn&#8217;t been solved, so here it goes again:</p>


<p>When selecting speakers for our conferences we always hunt for a video of a prospective speaker, unless we&#8217;ve seen them for ourselves in the flesh. If we cannot find a video, we do not invite the speaker, since we cannot guarantee to our audience that they are excellent presenters. Not all conferences do so, but we do.</p>

<p>For some new speakers this is a bit of a hurdle. Not an insurmountable one &#8212;  some local meet-ups and most conferences record their sessions &#8212; but it&#8217;s still an extra step. Local meet-up organisers take note: recording the sessions would be a huge service for your speakers &#8212; if you have the budget.</p>

<p>That was not what I wanted to talk about today, though. In the past months we conferred with quite a few developer relations departments about <a href="https://cssday.nl" class="external">CSS Day</a> and <a href="https://perfnow.nl" class="external">performance.now()</a>, and they all proposed a speaker that they didn&#8217;t have a video of. So we said No.</p>

<p>For developer relations departments, whose job it is to get their people to speak at conferences, this is a far more serious oversight than for individuals.</p>

<p>So my advice to developer relations departments is to organise a local meet-up, get all their unknown speakers to speak, record all sessions, and put them online. I mean, they must have the budget to do that once, right? It would be a great gift to conference organisers around the world, even to those that do not require videos.</p>

<p>And while you&#8217;re at it, hey, also invite that one local speaker that you think should get more conference invitations.</p>]]></description>
    </item>
    <item>
      <title>Column-filled grids: the issues</title>
      <link>https://quirksmode.org/quirksblog/2026/0507-cfg2.html</link>
      <pubDate>2026-05-07T12:00:00+02:00</pubDate>
      <description><![CDATA[<link rel="stylesheet" href="/quirksblog/column-fill.css">
<script src="/quirksblog/column-fill.js"></script>

<p>In <a href="0505-cfg1.html">part one</a> I created a technique to fill a grid column by column instead of row by row, while still setting a maximum number of columns. It works, but is brittle. Here we&#8217;ll discuss why it is brittle, and why lack of <code>children-count()</code> makes a theoretically superior technique for doing the same impossible.</p>

<h3>The technique</h3>

<div class="columnGrid column narrow tags" data-items="7" data-title="I want my tag list to resemble a book index."></div>

<p>The sidebar on my blog pages contains a list of tags. I want to first fill the left column alphabetically, then continue with the next &#8212; like an index in a book.</p>

<p>I use the following CSS:</p>

<pre>
.columnGrid {
	--size: 150px;
	--padding: 0.5em;
	display: grid;
	container-type: inline-size;
	grid-template-columns: repeat(auto-fit,minmax(var(--size),1fr));
	padding: var(--padding);
	
	@supports (order: sibling-count()) and (order: calc(1cqw/1px)) {
		grid-template-columns: 1fr;
		grid-auto-columns: 1fr;
	}
	
	& > * {
		--gridWidth: calc(100cqw - var(--padding) * 2);
		--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
		--rows: round(up,calc(sibling-count() / var(--maxColumns)));
		--row: calc(mod(calc(sibling-index() - 1),var(--rows)) + 1);
	
		grid-row: var(--row);
	}
}</pre>

<p>It works, but it has issues. Let&#8217;s talk about them.</p>

<h3>Issues</h3>

<div class="columnGrid narrow hole tags" data-items="7"  data-title="display: none in a regular grid"></div>

<div class="columnGrid narrow column hole tags" data-items="7"  data-title="... and with my technique"></div>

<p>Consider an item with <code>display: none</code>. In a regular grid it&#8217;s simply ignored. In my technique, however, it messes up the calculations.</p>

<p>Despite being hidden, the item is in the DOM, and counts for <code>sibling-count()</code> and <code>-index()</code>. Thus, the number of rows can be off. In the second example six items are visible, so it should have three rows. Instead, it has four because <code>sibling-count()</code> still counts seven siblings.</p>

<p>Worse, my technique assumes that the hidden item takes the row 2 / column 1 cell. But it doesn&#8217;t, and item 6, which is supposed to go in row 2 / column 2, goes into column 1 instead. That breaks the alphabetical order rather dramatically.</p>

<div class="columnGrid narrow column span tags" data-items="7"  data-title="CSS gets colspanned"></div>

<p>Something similar happens with a colspan. There is no way of detecting that 'CSS' now takes up two columns instead of one, and my technique breaks and the alphabetical order is off again.</p>

<p>CSS Grid by itself does a decent job of handling both situations. But I gave a bunch of absolute commands: <em>this</em> item should go <em>there</em>. I took away Grid&#8217;s ability to adjust, to compensate for problems, to balance things out. </p>

<p>I made myself responsible for handling these tricky situations &#8212; and I can&#8217;t. It&#8217;s not possible to say "don&#8217;t sibling-count an item that has <code>display: none</code>" or to correct <code>sibling-index/count()</code> for colspans and rowspans.</p>

<h3>Working with the grid</h3>

<p>CSS Grid does in fact have the tools to create a much more robust technique that handles the edge cases much better. I realised that after a day and a half of work, and decided to switch to a simpler, better technique.</p>

<div class="columnGrid narrow tags" data-items="7"  data-title="2 columns, 7 items. We need 4 rows."></div>

<p>By default, CSS Grid looks at the number of available columns and then creates enough rows to hold all the items. We as web developers only have to set the number of columns, and we&#8217;re in business. 

<p>This behaviour is caused by the declaration <code>grid-auto-flow: row</code>. It sort-of means "if you need more space, we&#8217;d prefer that you add rows." We tend to leave it alone because this is how we expect grids to work.</p>

<div class="columnGrid ideal narrow tags" data-items="7"  data-title="3 rows, 7 items. We need 3 columns."></div>

<p>But there&#8217;s also <code>grid-auto-flow: column</code>. Now CSS Grid looks at the available number of rows and then creates enough columns to hold all the items.</p>

<pre>
.columnGrid.withTheGrain {
	grid-template-rows: repeat(3,auto);
	grid-auto-flow: column;
	grid-auto-columns: 1fr;
}
</pre>

<div class="columnGrid narrow ideal hole tags" data-items="7"  data-title="With grid-auto-flow: column and display: none."></div>

<div class="columnGrid narrow ideal span tags" data-items="7"  data-title="With grid-auto-flow: column and colspans."></div>

<p>This, I realised, is the solution. CSS Grid has all kinds of clever defaults to paper over our issues. We just have to allow it to do its job.</p>

<p>We shouldn&#8217;t give CSS Grid a bunch of detailed orders, but hand-wavingly tell it to lay out these items column-wise as well as it can.</p>

<p>In the quick proof-of-concept I made, the one shown here, I hard-coded the three rows and it worked fine.</p>

<p>I still wanted to set a number of <em>columns</em>, though, and use the variables to calculate the number of rows. No problem, right? I already wrote that calculation, right? I just have to plug it into the new grid code, right?</p>

<pre>
.columnGrid.withTheGrain {
	--gridWidth: calc(100cqw - var(--padding) * 2);
	--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
	--rows: round(up,calc(sibling-count() / var(--maxColumns)));
	/* We don't need --row */

	grid-template-rows: repeat(<strong>var(--rows)</strong>,auto);
	grid-auto-flow: column;
	grid-auto-columns: 1fr;
}
</pre>

<p>Wrong.</p>

<h3>CSS variable scope</h3>

<p>CSS variables are scoped to the context in which they are evaluated. And we just changed that context. That has consequences.</p>

<p>If you&#8217;re not sure what that means, concentrate on <code>sibling-count()</code>. <em>Whose</em> siblings are we counting? Whose siblings <em>should</em>  we  be counting?</p>

<p>Well, we <em>should</em> be counting the siblings of the grid items. That gives us the total number of items we need for the calculation. The original technique did so.</p>

<pre>
.columnGrid {
	display: grid;
	
	& > * {
		--gridWidth: calc(100cqw - var(--padding) * 2);
		--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
		--rows: round(up,calc(sibling-count() / var(--maxColumns)));
		--row: calc(mod(calc(sibling-index() - 1),var(--rows)) + 1);

		grid-row: var(<strong>--row</strong>);
	}
}
</pre>

<p>The individual grid items used the variable <code>--row</code>. Thus, <code>--row</code> is evaluated in the context of a grid item, as are all the variables it depends on. In particular, <code>sibling-count()</code> counts the grid item&#8217;s siblings, and <code>sibling-index()</code> yields the grid item&#8217;s index.</p>

<p>But the new technique changes that:</p>

<pre>
.columnGrid.withTheGrain {
	--gridWidth: calc(100cqw - var(--padding) * 2);
	--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
	--rows: round(up,calc(sibling-count() / var(--maxColumns)));
	/* We don't need --row */

	grid-template-rows: repeat(<strong>var(--rows)</strong>,auto);
	grid-auto-flow: column;
	grid-auto-columns: 1fr;
}
</pre>

<p>Now the grid <em>container</em> uses the variable <code>--rows</code>, and it, as well as the other variables it depends on, are evaluated in the context of the grid container. In particular, <code>sibling-count()</code> now counts the grid <em>container</em>&#8217;s siblings.</p>

<p>And that&#8217;s wrong. I don&#8217;t care how many siblings the container has, but that useless number is force-fed into our formula and yields gibberish. Garbage in, garbage out.</p>

<pre>
.columnGrid.withTheGrain {
	--gridWidth: calc(100cqw - var(--padding) * 2);
	--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
	--rows: round(up,calc(<strong>children-count()</strong> / var(--maxColumns)));
	/* We don't need --row */

	grid-template-rows: repeat(<strong>var(--rows)</strong>,auto);
	grid-auto-flow: column;
	grid-auto-columns: 1fr;
}
</pre>

<p>In the new context we don&#8217;t need <code>sibling-count()</code>. Instead, we need <code>children-count()</code>. That would give us the number of grid items, and our formula would work once more.</p>

<p>But there&#8217; a tiny problem with <code>children-count()</code>: despite developer pressure it doesn&#8217;t exist. Well, an <a href="https://github.com/w3c/csswg-drafts/issues/11068" class="external">issue</a> exists, and since I take issue with its lack of existence I&#8217;m going to add a comment.</p>

<p class="smaller">There is a second problem: finding the width of the grid container. In the original version I could made it a queryable container so I could use a simple <code>100cqw</code> in the context of the grid item. But due to the change of evaluation context I now have to measure the width of the element from the context of that same element, and container queries don&#8217;t do that. Temami Afif <a href="https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/" class="external">solved that problem</a>, but if I were to use his technique I would have to fully understand it &#8212; that&#8217;s my rule for this blog. I&#8217;m not sure if I want to go on yet another long digression, so I&#8217;m secretly relieved I don&#8217;t have to.</p>

<p>That&#8217;s why we can&#8217;t use the superior version without <code>children-count()</code>, and we&#8217;re left with the brittle version that works in simple cases but will fail in more complicated ones. It&#8217;s a pity, but it cannot be helped.</p>

<p>My use case is really simple: I just want a simple grid with no hidden or colspanned items, no complicated. That will work. If you want the same, hey, use <a href="0505-cfg1.html">the original version</a>. If you want the better version, wait for <code>children-count()</code>.</p>]]></description>
    </item>
    <item>
      <title>CSS Day tickets: normal or late-bird?</title>
      <link>https://quirksmode.org/quirksblog/2026/0506-cssday.html</link>
      <pubDate>2026-05-06T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>The twelfth <a href="https://cssday.nl" class="external">CSS Day</a> conference will take place on 11th and 12th of June &#8212; that&#8217;s in slightly more than a month. As usual, it&#8217;s unmissable if you&#8217;re into CSS: <a href="https://cssday.nl/attendees.html" class="external">everybody is there</a> and all topics will be discussed &#8212; often until late.</p>

<p>Want to see <a href="https://cssday.nl/speakers.html" class="external">Adam Argyle and Kevin Powell and Lea Verou and Eric Meyer</a> and all the rest? Not only for a session but also for a hallway discussion? Then come.</p>

<p>But you should be quick. Right now <a href="https://cssday-2026.eventstack.shop/" class="external">regular tickets</a> cost €675 + VAT, but that price will rise to €750 + VAT in a week. 13th of May is the last day of normal tickets; the late-bird prices will kick in on Thursday 14th of May. (Why this price hike? It&#8217;s essentially a lateness tax. <a href="/quirksblog/2026/0407-conferences.html">I wrote about it</a> a few weeks back.)</p>

<p>So if you want to come &#8212; and you do &#8212;  better <a href="https://cssday-2026.eventstack.shop/" class="external">grab</a> a regular ticket now and save some money.</p>]]></description>
    </item>
    <item>
      <title>Column-filled grids: the technique</title>
      <link>https://quirksmode.org/quirksblog/2026/0505-cfg1.html</link>
      <pubDate>2026-05-05T12:00:00+02:00</pubDate>
      <description><![CDATA[<link rel="stylesheet" href="/quirksblog/column-fill.css">
<script src="/quirksblog/column-fill.js"></script>
<style>
body {
	max-width: 100vw;
}
</style>


<p>I have a grid that I want to fill with items, not row by row, as usual, but column by column. At the same time, I want to set a maximum number of <em>columns</em>, and add rows as needed, as a regular row-by-row grid does. To do so I created the technique described here.

<p>Then I noted it was pretty brittle, and figured out why. I found I could not make it more robust because <code>children-count()</code> is not yet supported. So we&#8217;re stuck with the sub-optimal version &#8212; for now.</p>

<p>This is a two-parter. This first part describes the brittle solution that works right now, and <a href="0507-cfg2.html">the second</a> the robust solution that doesn&#8217;t yet work.</p>

<p>Thanks to <a href="https://css-articles.com/" class="external">Temami Afif</a>, <a href="https://dev.to/janeori" class="external">Jane Ori</a>, <a href="https://kizu.dev/" class="external">Roman Komarov</a>, and <a href="https://amitsh.com/website/" class="external">Amit Sheen</a> for their help with several aspects of this mini-series.</p>

<h3>The problem</h3>

<div class="columnGrid narrow tags" data-items="7" data-title="Is this an alphabetical order ... or plain weird?"></div> 

<p>The sidebar on my blog pages contains a list of tags. I use a simple grid because the list can have either one or two columns, depending on the width of the sidebar. Simple grid is simple: it places the items in rows; the first two items in the first row, the second two in the second row etc. That&#8217;s how grids work.</p>

<div class="columnGrid column narrow tags" data-items="7" data-title="I want it to resemble a book index ..."></div>

<p>I don&#8217;t like that. The tags are in alphabetical order, and intuitively I want to first fill the left column alphabetically, then continue with the next &#8212; like an index in a book.</p>

<p>At the same time, I want to set a number of <strong>columns</strong> in a manner similar to <code>auto-fit</code>, and derive the necessary number of rows from that.</p>

<div class="columnGrid column tags" data-items="7" data-title="... and use any number of columns without losing the effect."></div>

<h3>The technique</h3>

<p>Here&#8217;s how you do it. The CSS variables determine the number of columns <code>auto-fit</code>-style and then derive the necessary number of rows. It&#8217;s this number of rows that we pass on to the grid, hoping to get the right number of columns back. (We may not.)</p>

<pre>
.columnGrid {
	--size: 150px;
	--padding: 0.5em;
	display: grid;
	container-type: inline-size;
	grid-template-columns: repeat(auto-fit,minmax(var(--size),1fr));
	padding: var(--padding);
	
	@supports (order: sibling-count()) and (order: calc(1cqw/1px)) {
		grid-template-columns: 1fr;
		grid-auto-columns: 1fr;
	}
	
	& > * {
		--gridWidth: calc(100cqw - var(--padding) * 2);
		--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
		--rows: round(up,calc(sibling-count() / var(--maxColumns)));
		--row: calc(mod(calc(sibling-index() - 1),var(--rows)) + 1);
	
		grid-row: var(--row);
	}
}</pre>

<p>You could hard code <code>--maxColumns: 3</code> or whatever value you like. If you do, you don&#8217;t need <code>container-style</code> or <code>--gridWidth</code>.</p>

<p>Here are a regular grid and a column-filled grid in action:</p>

<div class="columnGrid" data-items="14" data-title="A normal auto-fit grid. Resize to six columns to see an interesting difference." style="max-width: 100%"></div>

<div class="columnGrid column" data-items="14" data-title="A column-filled grid. It mimics auto-fit columns decently, though not perfectly." style="max-width: 100%"></div>

<p>It looks great but there are rather a lot of caveats.</p>

<ul class="biglist">
	<li>It doesn&#8217;t yet work in <strong>Firefox</strong> because Firefox doesn&#8217;t support length-on-length division (<a href="https://dev.to/janeori/css-type-casting-to-numeric-tanatan2-scalars-582j" class="external">solvable</a>) and <code>sibling-index/count()</code> (unsolvable).</li>
	<li>You can set a <em>maximum</em> number of columns, but the grid may use fewer. This is logical once you think about it, and unsolvable. Also, it&#8217;s no big deal.</li>
	<li>The technique only works well if all grid items take up exactly one 1x1 cell, and none of them are hidden. It is this <strong>brittle</strong> because it doesn&#8217;t allow Grid to function normally.</li>
	<li>Using a superior technique with <code>grid-auto-flow: column</code> is impossible because you can&#8217;t find the grid container&#8217;s width (<a href="https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/" class="external">solvable</a>), and <code>children-count()</code> is not supported (unsolvable). More on that in <a href="0507-cfg2.html">part 2</a>.</li>
</ul>

<p>On the positive side, this is not something you can do with flexbox.</p>

<h3>The calculation</h3>

<p>So how does the calculation work?</p>

<pre>
--gridWidth: calc(100cqw - var(--padding) * 2);
--maxColumns: round(down,calc(var(--gridWidth) / var(--size)));
--rows: round(up,calc(sibling-count() / var(--maxColumns)));
--row: calc(mod(calc(sibling-index() - 1),var(--rows)) + 1);
</pre>

<ul class="biglist vars">

<li><code>gridWidth</code> establishes the width of the grid container minus the padding. For this to work the grid container needs to be a queryable container.</li>

<li><code>maxColumns</code> divides the container width by the desired column size (in this example 150px) and rounds down. This is the maximum amount of columns the end result is going to use. (Yes, <em>maximum</em>. It could be fewer.)<br>
Unfortunately Firefox can&#8217;t handle length-on-length division yet, and it yields 0 as the number of columns.</li>

<li><code>rows</code> divides the total number of grid items (long live <code>sibling-count()</code>!) by the number of columns and rounds up. That&#8217;s how many rows we need in order to place all of the items.<br>
Firefox does not support <code>sibling-count()/index()</code>. That&#8217;s why, even if it would survive the previous line, it fails here.</li>

<li><code>row</code> is the row number for an individual grid item. This calculation relies on modulo: the remainder of dividing the item&#8217;s <code>sibling-index()</code> by the total number of rows.</li>
</ul>

<h4>Modulo?</h4>

<div class="columnGrid column narrow" data-items="9" style="--maxColumns: 3" data-title="Item number modulo item total yields row"></div>

<p>Suppose we have three rows and items 1 to 9. 1/3, 4/3, and 7/3 have a remainder of 1, and items 1, 4, and 7 go in row 1. Similarly,  2/3, 5/3, and 8/3 have a remainder of 2 and items 2, 5, and 8 go in row 2.</p>

<p>Finally, 3/3, 6/3, and 9/3 have a remainder of 0 but items 3, 6 and 9 go in row 3. Since we need 1, 2, and <em>3</em>, not 1, 2, and 0, add 1 to the modulo but subtract 1 from the index. Now it works.</p>

<h3>maxColumns</h3>

<p>And what&#8217;s the thing with the <em>maximum</em> number of columns? In grids, you can set either the number of columns or the number of rows exactly, but not both. CSS Grid needs some wiggle room somewhere.</p>

<div class="columnGrid" data-items="12"  data-title="Try giving this regular grid five rows. You can't."></div>

<p>In regular grids we set the number of columns, and leave it to the grid to decide on the number of rows. Some numbers of rows don&#8217;t occur. The regular grid above can&#8217;t have five rows.</p>

<p>Now let&#8217;s go to a column-filled grid. Although we start the calcuation with the number of columns, it&#8217;s actually the number of <strong>rows</strong> that we set. That means we can&#8217;t fully control the number of columns. Consider:</p>

<div class="columnGrid column narrow" data-items="9" style="--maxColumns: 3" data-title="--maxColumns: 3"></div>

<p>Here, we have nine items and <code>--maxColumns: 3</code>. No one will be surprised to hear that nine items in three columns need three rows. That&#8217;s what our CSS variables set, and CSS Grid does the obvious thing and generates three columns.</p>

<div class="columnGrid column narrow" data-items="9" style="--maxColumns: 4" data-title="--maxColumns: 4"></div>

<p>When we  go to <code>--maxColumns: 4</code>, things aren&#8217;t as neat. Two rows would be too few, since 4 columns * 2 rows = 8 item cells for 9 items. So we still need three rows, and that&#8217;s what our CSS variables set.</p>

<p>But now that we still use three rows for nine items, three columns is enough. We don&#8217;t need the fourth column, so CSS Grid doesn&#8217;t create one. <code>--maxColumns: 4</code> yields <em>three</em> columns.</p>

<h3>The @supports</h3>

<p>At the time of writing Firefox doesn&#8217;t support this technique. We have to make sure Firefox users still see a regular grid. It won&#8217;t be column-filled, but that can&#8217;t be helped. So let&#8217;s give them (and in fact everyone) just that:</p>

<pre>
grid-template-columns: repeat(auto-fit,minmax(var(--size),1fr));
</pre>

<div class="columnGrid column narrow standard" data-items="9" style="--maxColumns: 3" data-title="auto-fit gives 2 columns; the calculation 3"></div>

<p>The <code>auto-fit</code>ted columns take up all the available space. Thus, if the actual number of columns becomes greater than the number <code>auto-fit</code> calculates, there&#8217;s hardly any space left for those columns, as you can see in the example.</p>

<p>That&#8217;s where the <code>@supports</code> comes in. If the browser supports the technique, reset the number of columns to one  &#8212; CSS Grid will add them as needed. Also, set the width of the one defined column as well as any automatically-created ones to <code>1fr</code>.</p>

<pre>
@supports (order: sibling-count()) and (order: calc(1cqw/1px)) {
	grid-template-columns: 1fr;
	grid-auto-columns: 1fr;
}
</pre>

<p>Remember, Firefox lacks two features: <code>sibling-count()</code> and the ability to divide lengths. We check for both, and the syntax of <code>@supports</code> requires us to use a property: value pair. In this specific case the property doesn&#8217;t matter, but it still needs one. I picked <code>order</code> more or less at random, but you can use any property that accepts an integer: <code>z-index</code>, even <code>grid-row</code>.</p>
 
<h3>Working against the grid</h3>

<p>When I arrived at this point for the first time I admired the cleverness of the technique and myself, loved the way I forced CSS Grid to do my bidding.</p>

<p>Then I started having doubts. I bent CSS Grid out of shape, and holes were forming. If I&#8217;d work with, rather than against, CSS Grid the technique would become more robust. Unfortunately, as we&#8217;ll see in <a href="0507-cfg2.html">part two</a>, that is impossible.</p>]]></description>
    </item>
    <item>
      <title>performance.now() really cheap tickets</title>
      <link>https://quirksmode.org/quirksblog/2026/0429-perf.html</link>
      <pubDate>2026-04-29T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>The seventh <a href="https://perfnow.nl" class="external">performance.now()</a> conference will take place on 19th and 20th of November this year. As usual, it&#8217;s unmissable if you&#8217;re into web performance: everybody is there and all topics will be discussed &#8212; often until late.</p>

<p>Ticket sales will open in a few weeks, and there are a few <em>very</em> cheap early-bird tickets available &#8212; I&#8217;m talking €100 or €200 instead of €700 or €800.</p>

<p>However, in order to grab them you should <a href="https://perfnow.nl/subscribe" class="external">subscribe to the newsletter</a>, because that&#8217;s the only way the start of early bird will be announced. Not on this site, not on <a href="https://bsky.app/profile/perfnow.nl" class="external">social</a> <a href="https://front-end.social/@perfnow@mastodon.social" class="external">media</a>, not even on the conference site.</p>

<p>So <a href="https://perfnow.nl/subscribe" class="external">subscribe</a>. And see you in November!</p>]]></description>
    </item>
    <item>
      <title>From the archives: Impostor Syndrome</title>
      <link>https://quirksmode.org/quirksblog/2026/0422-impostor.html</link>
      <pubDate>2026-04-22T12:00:00+02:00</pubDate>
      <description><![CDATA[<p><a href="/blog/archives/2016/04/impostor_syndro.html">Exactly ten years ago</a> I wrote this vignette about impostor syndrome. Fun fact: after I published it I felt like an imposter for spelling "impostor". Here it goes:</p>

<p>Just now <a href="https://twitter.com/zeldman/status/723509179282010112" class="external">Zeldman tweeted a question</a> to which I <a href="https://twitter.com/ppk/status/723512593873293313" class="external">replied</a>. That reminded me of a story I want to share with you. Zeldman asked:</p>

<blockquote>
<p>Have you ever felt that you have no talent whatever? How often do you feel that way?</p>
</blockquote>

<p>What he describes is classic impostor syndrome. I&#8217;ve got it, you&#8217;ve got it, just about everybody&#8217;s got it. It&#8217;s the &#8220;just about&#8221; that I want to discuss today.</p>

<p>A few months back a conversation with friends turned to the subject of impostor syndrome. They didn&#8217;t know the term, but they recognized it and agreed they had it to a larger or smaller degree. Then a friend of mine who&#8217;s a doctor told us a story.</p>

<p>She told us that one time the conversation among her and her colleagues also turned to impostor syndrome. One doctor confessed he did <strong>not</strong> have it. He understood what the others were talking about, but he just didn&#8217;t feel that way. He was always sure of himself.</p>

<p>A few months after that conversation this doctor made a very serious medical mistake. Can&#8217;t remember if it was fatal or not, but it was major, and had consequences for the patient and the doctor himself.</p>

<p>Once she had told this story, my friends and I concluded that impostor syndrome actually serves an important function. It forces you to check and re-check your work, making sure you haven&#8217;t made any mistakes, consider different approaches, and generally be <em>critical</em> of yourself in a positive sense.</p>

<p>So cherish your impostor syndrome. Don&#8217;t trust people who don&#8217;t have it.</p>]]></description>
    </item>
    <item>
      <title>Demystifying block formatting contexts</title>
      <link>https://quirksmode.org/quirksblog/2026/0416-bfcs.html</link>
      <pubDate>2026-04-16T12:00:00+02:00</pubDate>
      <description><![CDATA[<style>

section.test {
	outline: 1px solid var(--maindark);
	hyphens: auto;
	display: flow-root;
		/* just to make sure the margin-top of the topmost paragraph 
			is between the border and the paragraph - without this
			display: flow-root it collapses with the margin of the last
			element before and outside the section, and there is no
			visible margin within the section. 
			Margin collapsing was a mistake. */
	
	& span {
		float: left;
		width: 200px;
		margin: 0.3em;
		margin-bottom: 0;
		margin-right: 0.7em;
		padding: 0.3em;
		border: 1px solid var(--maindark);
	}
	
	& p {
		background: var(--mainlight);
		margin: 0.3em;
		padding: 0.3em;
		
		&:first-of-type {
			margin-bottom: 1em;
		}
	}
	
	& *:is(p,span):before {
		content: ' This is not a block formatting context';
		display: block;
		font-style: italic;
		font-size: 90%;
		max-width: 100%;
		color: var(--main);
	}
	
	& span:before {
		content: ' This is a block formatting context because it floats';	
	}
	
	& .root {
		display: flow-root;
		
		&:before {
			content: ' This is a block formatting context because of display: flow-root';
		}
	}

	& .overflow {
		overflow: auto;
		max-height: 8em;
		
		&:before {
			content: ' This is a block formatting context because of overflow: auto';
		}
	}
}

p.image {
	width: 250px;
	float: left;
	margin-right: 25px;
	margin-top: 0.5em;
	font-size: 80%;
	color: var(--maindark);
	text-align: center;
	outline: 1px solid var(--maindark);
}

</style>

<p>I found an example of how we web developers wield CSS as black magic and reinforce its image as a weird and impossible language. Also, I learned to explain block formatting contexts.</p>

<p>Today, let&#8217;s demystify <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Display/Block_formatting_context" class="external">block formatting contexts</a> in two ways:</p>

<ol>
	<li>To <strong>establish</strong> a block formatting context, don&#8217;t use <code>overflow</code> or <code>container-type</code> or any other black magic. Instead, use the "semantically correct" <code>display: flow-root</code>.</li>
	<li>To <strong>explain</strong> a block formatting context, tell people that it&#8217;s an element that&#8217;s <em>ready to scroll</em>.</li>
</ol>

<h3>Solving a bug</h3>

<p class="image"><img src="/quirksblog/pix/flow-root6.png"> Paragraph doesn't work!</p>

<p>In my <a href="/quirksblog/2026/0414-speakers.html">previous post</a> I placed the CSS Day teaser image at the top of my post, where the floating bar that I call "floater" has been since at least 2003. Narrowing the page made the image fall to the bottom of the floater. We&#8217;ve all encountered this; it&#8217;s annoying.</p>

<p>I did what I do best. I exuded a noise on the Socials that the uninitiated take for plaintive mooing but is in fact a powerful summoning spell. Three friendly spirits, <a href="https://css-articles.com/" class="external">Temami Afif</a>, <a href="https://www.miriamsuzanne.com/" class="external">Miriam Suzanne</a>, and <a href="https://dbaron.org/" class="external">David Baron</a>, appeared out of thin air to enlighten me.</p>

<p>Temami <a href="https://front-end.social/@css/116392607111039661" class="external">told me</a> to use <code>container-type: inline-size</code>. This solved the bug. The paragraph now took the existence of the floater into account when calculating its width. No more falling to the bottom.</p>

<p class="image"><img src="/quirksblog/pix/flow-root4.png"> Fixed. But how? Why?</p>

<p>But why? What sort of black magic was this? Some sort of obscure side effect of container queries that I&#8217;m not acquainted with? (My experience with container queries so far is rather limited and basic.)</p>

<p>Then I had an insight and replaced the <code>container-type</code> with <code>overflow: auto</code>. Yup, that still worked.</p>

<p>Clearly, in addition to creating a container, <code>container-type</code> also engages the ... thingy ... the whatsitcalled that also works with <code>overflow: auto</code> ... the ... the <em>block formatting context</em> (or BFC).</p>

<p>I tested this by replacing the <code>overflow</code> with <code>display: flow-root</code>. It still worked fine. So making the paragraph a BFC solves the issue. Better still, I now sort-of understood what was going on, where I had been bewildered when staring at <code>container-type</code>.</p>

<h3>CSS semantics</h3>

<p>This led to <strong>demystification #1</strong>: if you want to make something a BFC, just do it. Use <code>display:flow-root</code>. Be "pedantic-semantic" about it. Don&#8217;t use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Display/Block_formatting_context" class="external">any of the other declarations</a> that establish a BFC as a side effect. The developers who inherit your code will thank you for it.</p> 

<p>I understand why people use other declarations. While figuring out what was going on I intuitively reached for the <code>overflow: auto</code> I&#8217;ve been using for more than twenty years to keep floats in check.</p>

<p>But that was the wrong reaction.</p>

<p>I didn&#8217;t want the paragraph to scroll. I didn&#8217;t want it to become a queryable container, either. Thus <code>overflow</code> and <code>container-type</code> were semantically incorrect. They reek of black magic and reinforce the image of CSS as this weird language that is impossible to learn.</p> 

<ul>
	<li><strong>Dev guy #1</strong>: You have to give the paragraph with the image scrollbars in order to solve the float bug. How weird is that??!?</li>
	<li><strong>Dev guy #2</strong>: Yeah, CSS sux!!!</li>
	<li>They solemnly vow to go even fuller stack.</li>
</ul>


<h3>Getting ready to scroll</h3>

<p>By this time I kind-of vibe-grokked why the paragraph should be a BFC, but didn&#8217;t yet understand the exact reasons. David Baron came <a href="https://bsky.app/profile/dbaron.org/post/3mjd6na4atc2i" class="external">to the rescue</a> and handed me <strong>demystification #2</strong>.</p>

<p>A BFC is <em>a block that&#8217;s ready to scroll</em>. (Well, it&#8217;s more complicated, but this is a good first approximation for newbies.) Take a look at this example.</p>

<section class="test">
<p><span>The floating <code>span</code> that pushes content, or entire blocks, to the side, depending on the configuration</span>
Contents of the first paragraph that will move to make place for the floater in any case &#8212; but will the paragraph block itself move as well? Only if it&#8217;s a <code>flow-root</code>.</p>
<p>Contents of the second paragraph that may also make place for the floater, depending on the exact circumstances. The paragraphs have a background colour in order to make them easily trackable.</p>
</section>

<p>Are the two paragraphs with the light background colour, the ones that say they are not a block formatting context, ready to scroll? That is, what would happen if the paragraph itself would have a scrollbar?</p>

<ul>
	<li>If the first paragraph would scroll, what would happen to the floating <code>span</code>? Would it be slowly pulled inside the paragraph, pixel by pixel? And if it did, what would happen to the second paragraph?</li>
	<li>And what about the second paragraph? If it would scroll, would its content wrap around the floating <code>span</code>, one line after the other as they scroll into reach?</li>
</ul>

<p>You know the answer, I know the answer. That&#8217;s not how it works. That&#8217;s what I mean when I say these elements are not ready to scroll.</p>

<p>Let&#8217;s give the first paragraph an <code>overflow: auto</code>. You know how it&#8217;ll react, and so do I, but you may not have thought of it as <em>getting ready to scroll</em>. I sure didn&#8217;t. Yet that&#8217;s exactly what happens. The floating <code>span</code> is retracted into the paragraph so that it can scroll without influencing anything outside itself.</p>

<section class="test">
<p class="overflow"><span>The floating <code>span</code> that pushes content, or entire blocks, to the side, depending on the configuration</span>
Contents of the first paragraph that will move to make place for the floater in any case &#8212; but will the paragraph block itself move as well? Only if it&#8217;s a <code>flow-root</code>.</p>
<p>Contents of the second paragraph that may also make place for the floater, depending on the exact circumstances. The paragraphs have a background colour in order to make them easily trackable.</p>
</section>

<p>The effect becomes even clearer when we just use <code>display: flow-root</code>. Now the paragraph stretches up to accommodate the floating <code>span</code>. That makes it ready to scroll.</p>

<section class="test">
<p class="root"><span>The floating <code>span</code> that pushes content, or entire blocks, to the side, depending on the configuration</span>
Contents of the first paragraph that will move to make place for the floater in any case &#8212; but will the paragraph block itself move as well? Only if it&#8217;s a <code>flow-root</code>.</p>
<p>Contents of the second paragraph that may also make place for the floater, depending on the exact circumstances. The paragraphs have a background colour in order to make them easily trackable.</p>
</section>

<p>It won&#8217;t actually scroll, but that doesn&#8217;t matter. There are plenty of declarations that establish a BFC without scrolling, such as <code>overflow: hidden</code>. It should just be <em>possible</em> to scroll it. </p>


<p>Finally, what about the second paragraph? If we make it a BFC, it also gets ready to scroll.<p>

<section class="test">
<p><span>The floating <code>span</code> that pushes content, or entire blocks, to the side, depending on the configuration</span>
Contents of the first paragraph that will move to make place for the floater in any case &#8212; but will the paragraph block itself move as well? Only if it&#8217;s a <code>flow-root</code>.</p>
<p class="root">Contents of the second paragraph that may also make place for the floater, depending on the exact circumstances. The paragraphs have a background colour in order to make them easily trackable.</p>
</section>

<p>In this case, getting ready to scroll means making sure it&#8217;s not able to be influenced by the float. The only real option of doing that is to move the entire block out of the way of the float. So that&#8217;s what happens, and it was this aspect of BFCs that solved the problem I had at the start of this article.</p>

<p>You&#8217;ll have to be much more specific and explicit if you aim for completeness, but a quick-and-dirty "getting ready to scroll" should be enough to explain the core concept of BFCs.</p>]]></description>
    </item>
    <item>
      <title>CSS Day final speakers</title>
      <link>https://quirksmode.org/quirksblog/2026/0414-speakers.html</link>
      <pubDate>2026-04-14T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>The final CSS Day speakers have been announced: 
<a href="https://meyerweb.com/" class="external">Eric Meyer</a>,
<a href="https://jakearchibald.com/" class="external">Jake Archibald</a>,
and <a href="https://csswizardry.com/" class="external">Harry Roberts</a>. With them on board, the line-up is now complete and we&#8217;re good to go.</p>

<p><a href="https://cssday.nl" class="external"><img src="/quirksblog/pix/cssday-2026.png"></a></p>

<p><a href="https://cssday.nl" class="external">CSS Day</a> will take place on 11th and 12th of June in Amsterdam. As always, our venue is the <a href="https://cssday.nl/venue.html" class="external">Zuiderkerk</a>. If you&#8217;re thinking of buying a ticket, please note the following:</p>

<ul>
	<li>From 1st of May on, that's in two weeks, <strong>only Stripe</strong> payments will be possible. Buying tickets on invoice will become impossible. That matters to some large companies.
	<li>Two weeks later, on 14th of May, the ticket price will go up from <strong>€675</strong> to <strong>€750</strong>. Orders of five tickets or more will still get a <strong>10% discount</strong>, but over the higher price.
</ul>

<p>I like the <a href="https://cssday.nl/speakers.html" class="external">speaker list</a>, but I&#8217;m biased because I created it. But maybe even more important is our <a href="https://cssday.nl/attendees.html" class="external">attendee list</a>. Take a look and you will probably find a few people you follow &#8212; and they&#8217;ll be there not as speakers but as attendees. That means they will have time to geek out about CSS with you and you&#8217;ll become their friend, not their follower. That's the value of CSS Day.</p>

<p><a href="https://cssday.nl/tickets.html" class="external">See you</a> there?</p>]]></description>
    </item>
    <item>
      <title>Safari :has(:empty) bug</title>
      <link>https://quirksmode.org/quirksblog/2026/0410-bugreport.html</link>
      <pubDate>2026-04-10T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>This week I spent too much time on a <a href="/browserbugs/safari-has-empty.html">Safari :has(:empty) bug</a> &#8212; my first bug report in at least five years. I did it mostly to prove I could still do it. The Safari team <a href="https://github.com/WebKit/WebKit/pull/62402" class="external">already fixed</a> the bug, so this report helped a bit.</p>

<p>A month ago I I created a <a href="spelverkoop/" hreflang="nl">sales page</a> to sell about two-thirds of my board game collection. (If you're in Amsterdam and want to buy one of them, hey, let's talk!) I show sold games in a separate table until they&#8217;re actually collected &#8212; and I decided to hide that table if it contained no data.</p>

<pre>
main:has(tbody:empty){
	display: none;
}</pre> 

<p>Isn't CSS cool nowadays?</p>

<p>Except in Safari (Mac and iOS). No content at all was visible. Unless you resized the page. Vertically.</p>

<p>It says something shameful about my testing habits that it took me a month to notice. Then again, it was browser bug #32820 or so in my professional career.</p> 

<p>(Meanwhile I removed the offending rule, so you can only see the bug on the <a href="/browserbugs/safari-has-empty.html">bug report page</a>.)</p>

<h3>The bugs these days</h3>

<p>The problem is, I'm just not very impressed by today&#8217;s browser bugs. The ones we had 25 years ago, when Netscape and IE battled it out, now <em>those</em> were the real deal! And they remained bugs for <em>years</em>. Nobody fixed bugs within 24 hours.</p>

<p>But today&#8217;s web developers start crying as soon as pretty much anything goes even slightly wrong, and I simply don&#8217;t take the silly yelling youngsters who encounter My First Very Minor Browser Cncompatibility seriously. And no, I&#8217;m not old.</p>

<p>This goes beyond a minor incompatibility, though. A selector that should work doesn&#8217;t work. On <em>my</em> page. Somebody really should do something.</p>

<p>Was that somebody going to be me? Did I feel like solving the bug, as I did in the good old days? Or would I just remove the offending selector and let it be?</p>

<p>To my surprise, I wanted to prove I could still solve a browser bug. I mean, I have this huge brain lobe dedicated to browser incompatibilities, but it kind of atrophied in the past ten years. Could it still handle a juicy bug?</p>

<p>It could. Actually, the process went fairly smoothly and was enjoyable, and I even found a partial workaround. 

<h3>Findings</h3>

<p>My findings, summarised:</p>

<ul>
	<li><code>y:has(x:empty)</code> or <code>:has(x:empty) y</code> is checked for emptiness only once, between DOMContentLoaded and load.
	<li>Emptiness is resolved incorrectly if element <code>x</code> is filled by a JavaScript onload AND the body  overflows the html vertically, but not horizontally.
	<li>If this is the case AND element y has CSS lengths in viewport units AND element x is currently filled, resizing the window updates the styles of element y &#8212; permanently.
	<li>A special case applies if <code>y</code> has <code>display: none</code>, as it had in my original page. See <a href="/browserbugs/safari-has-empty.html">the report</a>.
</ul>

<p>From the <a href="https://github.com/WebKit/WebKit/pull/62402" class="external">fix</a> it appears that the problem was simply that <code>:has</code> was not updated after the <code>:empty</code> state changed. Initially I assumed that that was the case, but the wealth of extra bits and pieces of bug I found made it appear it was much more complicated. (I&#8217;ve never been able to figure out which of these details are important and which ones merely obscure the truth, but I tend to err on the side of over-reporting.)</p>

<h4>Workaround</h4>

<p>Adding a non-<code>:has</code> selector such as the one below fixes the bug. It needs a CSS declaration, but that can just be a variable that you never use. It has to contain the <code>:empty</code> element and select your <code>y</code> element, like this:</p>


<pre>
x:empty ~ * {
	--bug: 'solved';
}

:has(x:empty) y {
	// works!
}
</pre>

<p>This may not always work. In particular, it doesn't work for my original use case since I can&#8217;t write a selector for <code>main</code> that also touches <code>tbody:empty</code> without using <code>:has</code>.</p>

<pre>
main tbody:empty {
	// doesn't work
	// selects tbody instead of main
}

main:has(tbody:empty){
	display: none;
}</pre> 

<p>In order to truly solve the problem you&#8217;d have to remove the <code>:has(:target)</code>, I&#8217;m afraid &#8212; so that&#8217;s what I did on my page. All the better that the bug has been solved.</p>

<h3>Time and money</h3>

<p>The process took me about eight hours: six for the research, and two for the report writing interspersed with some research &#8212; for instance, the workaround came from an idea I had fairly late in the writing process. And that doesn&#8217;t count writing this blog post.</p>

<p>Although I don&#8217;t regret spending a working day on this bug, it&#8217;s not something I&#8217;m going to do a lot of. I mean, conference catering proposals don&#8217;t check themselves, and sponsors and speakers need gentle reminders every now and then. This week I did less conference work than I had planned.</p>

<p>And this sort of bug squashing doesn&#8217;t make you any money. That&#8217;s part of the problem &#8212; it always has been.</p>

<p>But still, it was fun. It&#8217;s good to be back.</p>]]></description>
    </item>
    <item>
      <title>Conference organising in 2026</title>
      <link>https://quirksmode.org/quirksblog/2026/0407-conferences.html</link>
      <pubDate>2026-04-07T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>If you've been following any conference organisers at all, you'll know that we have a  much tougher job than ever before and complain loudly. Tickets are more difficult to sell these days &#8212; not only because fewer people buy them, but also because they buy them much later.</p>

<p>Until a year ago my conferences were exempt from this trend, but starting with <a href="https://perfnow.nl/" class="external">performance.now()</a> 2025 ticket sales became a lot more difficult. perf 25 didn't sell out, not even close, and that was a first. It was the sponsors that pulled the conference through &#8212; thank you for that!</p>

<p>Now that we're at the half-way point for <a href="https://cssday.nl" class="external">CSS Day 2026</a> I can say we're definitely behind CSS 2025, though the situation is not as disastrous as for perf 25. It's quite conceivable sales will become better &#8212; these last three weeks are already better than the previous seven, and there <em>will</em> be late sales, no matter what. Still, my (as-yet unexplained) immunity to industry trends is over.</p>

<p>There are several lessons to be drawn from this.</p>

<p>From 2022 to about a year ago my theory was that CSS Day and performance.now() are focused, specialised conferences, while most of the complaining conferences were general ones. It seemed logical to me: specialise, and you'll attract specialists who won't go to a general web conference, and your ticket sales will remain strong. Now I find that this theory is not necessarily correct.</p>

<h3>Cash flow</h3>

<p>This year I'm implementing a solution that I've been talking about for ages with other conference organisers: more expensive late tickets. As an example, currently CSS 26 <a href="https://cssday.nl/tickets.html" class="external">tickets</a> are €675, but on 14th of May, a month before the conference, that price will go up to €750. See it as a lateness tax. I'm planning something similar for perf 26, but probably even more aggressive with three tiers instead of two.</p>

<p>I'm not very worried about sales effects: the number of people for whom €750 is a serious financial issue but €675 is not is probably negligible compared to people who have the budget anyway but are just late. (Or these might be famous last words, we'll see.)</p>

<p>Still, in addition to the total sales there's also the issue of sales timing. Right now I do not dare to order a barista or captioning for CSS Day. Attendees tend to really like them, but strictly speaking they're luxuries. When ticket sales are as unpredictable as they are now I balk at spending the extra ~€9K they would cost, though. If sales pick up considerably (or I find <a href="https://cssday.nl/sponsorships.html" class="external">sponsors</a>) I'll do it, but there's a practical time limit as well.

<p>From about a month before the conference it's no longer really possible to place an order, since my favourite captioners and baristas will have accepted other jobs. So it's not just about the size of the cash flow, but also about its timing. And that's what makes late ticket orders such a problem. It's quite possible that, in hindsight, I could have afforded a barista and captioning, but the ticket sales just came too late.</p>

<h3>Sponsors</h3>

<p>It's hard to find CSS Day sponsors. Fortunately that does not go for performance.now(): barista and captioning are already covered by sponsorship contracts &#8212; and it's possible that once more it's the sponsors, and not the ticket sales, that will pull the conference through.</p>

<p>The problem here should be obvious: more reliance on sponsors means they'll get a bigger say in the conference. My conferences don't do sponsored talks at all, and I hope that continues to be the case. The perf sponsors are a great bunch, and they're typically driven by the engineers in the company, not the marketing people. That helps. A lot. I don't see them trying to influence the schedule.</p>

<p>But ... well, I don't have to draw you a picture of what would happen if that changes.</p>

<h3>US extinction event</h3>

<p>One of the things I worry about is that whatever extinction event took out the US web conference circuit will also take place in Europe. But I'm not entirely sure what happened, so I'm not sure how to avoid it.</p>
		
<p>Fact is that there are WAY fewer web conferences in the US than there used to be. When I worked for the <a href="https://interledger.org/" class="external">ILF</a> I did some research for sponsorship purposes, and I found few survivors. In fact, this is how my first round of research on the Socials went:</p>

<ul>
<li><strong>Me</strong>: Hi, I'm looking for US web conferences. Know of any good ones?</li>

<li><strong>The world</strong>: [crickets]</li>

<li><strong>Me</strong>: ... </li>

<li><strong>The world</strong>: ... do you need more crickets?</li>

<li><strong>Me</strong>: ... nah ...</li>

<li><strong>Someone</strong>: Err ... Smashing?</li>
</ul>

<p>The joke is, of course, that <a href="https://smashingconf.com/" class="external">Smashing Conferences</a> is a European organiser. And they don't do a 2026 US conference.</p>

<p>I heard two explanations for the extinction that sort-of make sense to me. First, Covid. In general, Americans are much more worried about diseases than Europeans, and where Europeans flocked back to the conference circuit in 2022, Americans did not; or not to the same degree, I'm not sure.</p>

<p>Second, even before Covid the US conferences had become over-reliant on sponsors (read: VCs), and they basically only treated topics the sponsors wanted to see treated. Nowadays that means a lot of AI, some AI for diversity, supported by a solid helping of AI. In contrast, my two conferences (28 speakers) will probably see a single talk about AI.</p>

<p>(A third factor could be Trump, but that only explains why non-Americans don't want to go to the US. It shouldn't affect internal US conferences, and the extinction took place well before 2025.)</p>

<p>I can't speak for the truth of these theories. The last time I was in the US was at Smashing New York 2024, and that seemed like a perfectly normal web conference with happy attendees and only a single talk about AI &#8212; but it was organised by Europeans, not Americans, so it's probably not representative.</p>

<p>Or is it? Is it the destiny of the Europeans to ride to the aid of our beleaguered American colleagues, for rescue or revenge? Or am I being overly dramatic and in love with my own words after my <a href="20260403-back.html">blogging break</a>? Questions, questions...</p>

<p>For a while now I've been thinking about organising something performance-related in North America. I even did a little bit of work. But it all depends on how ticket sales go in the next few months. If they're bad I just don't dare to order (and pay for) anything.</p>

<p>The conference circuit is in a slump these days. That won't change as long as people don't buy tickets. And a good conference circuit is typically  something that you start to miss only when it's too late.</p>]]></description>
    </item>
    <item>
      <title>Back</title>
      <link>https://quirksmode.org/quirksblog/2026/0403-back.html</link>
      <pubDate>2026-04-03T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>Tap tap. Is this thing on? [... clears throat ...] Well, I'm back.</p>

<p>Who among you noticed I didn't blog for over four years? [No hands are raised. Nobody even pays attention.] Right.</p>

<p>The story is boring. I used to run Movable Type, an old Perl blogging system, from 2003, when I started blogging, to 2021, when my ISP shut down. My original plan for moving to another ISP failed miserably. With only days on the clock I had to jump to an essentially random ISP who didn't support Perl, but did offer Wordpress.</p>

<p>So now I was left with no blog, but with this big blue 'Install Wordpress' button (div?) that both enticed and frightened me. I never felt comfortable pressing it. I didn't want Wordpress to take over the non-blog part of this site &#8212; I still run that by hand-coding HTML and FTP-ing it to the server &#8212;  and it can't generate static HTML pages out of the box.</p> 

<p>Then <a href="https://paulvanbuuren.nl/" class="external">Paul van Buuren</a> told me a partial install was possible, and there was a plugin for creating static pages. That sounded a lot better. So I decided to do a partial install for blogging purposes only, with the static page plugin &#8212; later.</p>

<p>But later became never.</p>

<p>Then I switched ISPs again, to get rid of the passive-aggressive tone of my old one, and because <a href="https://jvhellemond.nl/" class="external">Jan van Hellemond</a>, who now helps me out with the conferences, recommended it for various reasons. I moved over the <a href="https://cssday.nl" class="external">conference</a> <a href="https://perfnow.nl" class="external">sites</a> a few weeks ago, and this week QuirksMode followed.</p>

<p>I asked Jan for advice on a blogging system, and this week it turned out he'd created a simple one himself. It's so new it doesn't even have a name. Let's see if it works. If you can read this it does.</p>

<p>So here I am, typing away in my static HTML file that I've always used to preview my blogposts in. Once I'm done I'll publish it with the newfangled publication system (really only an upload and a shell command). It's still a confusing process because it's so new to me, but it feels better than a massive Wordpress install.</p>

<p>And I may have something useful to say about CSS, once I've done some tests.</p>

<p>And BBEdit still remembers the macros I need for my blogging, such as Shift+Option+A for an external link, and Option+- for an em-dash. </p>

<p>It's good to be back.</p>]]></description>
    </item>
    <item>
      <title>Thidrekssaga XII - XIV</title>
      <link>https://quirksmode.org/quirksblog/2026/0402-ths.html</link>
      <pubDate>2026-04-02T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>In the four blogless years I concluded my guided tour through the Thidrekssaga with parts <a href="/ths/firstread/grimhild.html">XII</a>, <a href="/ths/firstread/return.html">XIII</a>, and <a href="/ths/firstread/death.html">XIV</a>..</p>

<p>These parts treat Grimhild's plot to kill her brothers in revenge for their killing of Sigfrid, the return of Dietrich von Bern from exile, and the death of a few main characters of the saga, ending with two version of Dietrich's own death. <span class="smaller">(Incidentally, I think I'm ready to defend the theory that the first of those two stories was written in Aachen at the end of Charlemagne's reign. Writing a proper scholarly article takes a lot of time, though.)</span></p>

<p>My <a href="/blog/archives/2020/10/side_project_th.html">original purpose</a> was not only to continue my research, but also to learn a bit of PHP and to solve the thorny issues of footnotes (which really are sidenotes) on the Web. My 2020 solution, which you can for instance see <a href="/ths/saga.php?ch=53-56">here</a>, was OK enough for a first try, but far from perfect. In particular, you will see that the current sidenotes can overlap one another; they're all positioned absolutely.</p>

<p>I'm hoping Anchor Positioning can help solve this; there was something with 'place positioned element here, but below any other positioned element', right...?</p>]]></description>
    </item>
    <item>
      <title>The old blog</title>
      <link>https://quirksmode.org/quirksblog/2026/0401-old.html</link>
      <pubDate>2026-04-01T12:00:00+02:00</pubDate>
      <description><![CDATA[<p>Go <a href="/blog/archives/2021/09/">here</a> to continue reading
the old 2003-2021 blog in reverse-chronological order.</p>]]></description>
    </item>

  </channel>
</rss>
