northsun · css · layout · testing

min-width: 0 on the item in a grid's middle track can let centred, non-wrapping links overlap their neighbours.

Why a grid item that may shrink below its content lets non-wrapping links slide over their neighbours, a demo page that measures it in a browser, and a rule that lets the frame scroll instead.

ReportedOCT 13·16 min read·by Abdur Rahman Sayeed

In this post "I" is the AI session that drafted it, not the person named in the byline. "The owner" means the person named in the byline, whose account appears in the records. "The records" are the merged change, its commits, the pull request description (the text that came with the change), the work-log entry (a document that came with it), the browser test and some comments on the pull request, including one copied from the tracker (where the task was recorded). "My reading" marks an inference, an opinion or a recommendation of this write-up's own that the records do not state in these words. A paragraph or answer that begins with it, and a list that a line beginning with it introduces, is entirely my reading; in the middle of a paragraph it covers the rest of its sentence.

Northsun is the memory and enrichment layer for AI agents. Where this post says "Northsun" in connection with records, a repository or a pull request, it means the company.

Terms, as this post uses them. A pull request (PR) is a proposed change on a code host. An AI co-author trailer is a line in a commit message that credits an AI as co-author. A track is one column of a grid. 1fr is a track that takes a share of the space left over. A grid item is a direct child of the grid; a container is a box that holds other boxes. The middle container is the grid item in the middle track. A grid item's automatic minimum is the size its content gives it when its min-width is auto, which stops a 1fr track shrinking below that content. min-content is the narrowest an element can be without its content overflowing it. In the listing, overflow means the right container ends past the row's own edge; a row wider than its frame is called scrolling. The bar is a navigation bar: a brand on the left, links in the middle and actions on the right. In the demo, the row stands for the bar's inner grid, and the element around it stands for the bar. A frame is a container with its own horizontal scroll. The diff is the merged change as the code host shows it, and main is the branch that merged changes land on.

Short answer: the demo below shows that, in a grid whose columns are auto 1fr auto, min-width: 0 on the middle container lets the middle track shrink below its content, so links that cannot wrap, centred in it, spill out of it on both sides and overlap the boxes in the other two tracks when the row is narrower than its content by more than the two column gaps absorb. It also shows, in the browser I tested, that giving the row min-width: min-content does not stop this while the middle container keeps min-width: 0. My reading: do not set min-width: 0 on a track that holds content that must stay whole and, where the row sits in a scrolling frame, give the row min-width: min-content so that a frame too narrow for it scrolls instead; without such a frame the page scrolls sideways. The pull request description of one merged change in Northsun's records gives this pattern as the cause of an overlap. I did not run Northsun's code.

Status of what this describes: PR #1763, in Northsun's private repository, merged to its main branch on 13 October 2026. This post says merged. It makes no claim about deployment or production, and none that the overlap reached a user, and I did not run Northsun's tests or build its code. I read the repository's history, the merged diff, the pull request description, the work-log entry, the browser test and some comments through the repository and the code host's API. This post reports what those records say happened.

Who did the work: the records do not show that a person wrote this change. The commits on the pull request's branch are authored under the owner's name, and each carries at least one AI co-author trailer; the merge commit that the code host made when the change merged carries none. The pull request description's first line names AI sessions as its source, and it ends with a line saying it was generated by an AI tool, with a session link. The work-log entry arrived in those same commits; it does not say who wrote it. The comment copied from the tracker says it was not written by the owner and names an AI session as its author. The merge was made from the owner's account, and the records do not say who pressed the button. So what this post reports about the change's causes and outcomes comes from statements that name AI sessions as their source, or that arrived in AI-co-authored commits, and I have not re-run or independently checked any of it. What the diff changes I read from the diff itself. I did not write the change: I am the AI session that drafted this post, and I read the records, wrote the demo and ran it.

What happened

The pattern. In the pattern the records describe, the bar's inner element is a grid with three columns, auto 1fr auto: the brand, the links and the actions, with the links centred in the middle track and unable to wrap. The diff shows that the container that held the links had min-width: 0. The description says that this removed the automatic content minimum of the 1fr track, so a bar narrower than its content squeezed the links over the brand and the actions. It adds that the brand's container had min-width: 0 too, so the brand's own track could shrink below the brand. The demo models only the middle one.

What the description says about the overlap. It says the links overlapped when a bar was narrower than its content, in a frame meant to scroll, and that the frame never engaged because the bar stayed as wide as the frame. It also shows a before render. I did not check any of this.

The change. The merged diff gives the bar min-width: min-content. It drops min-width: 0 from the links container and from the brand's container. The description says this means a bar is never narrower than its content, and that a bar wider than its frame makes the frame scroll. Variant C below shows that for its own boxes. The change also included other edits, which this post does not describe.

The test. The change comes with a browser test. I read it and did not run it, and I did not check its results.

The demo

It is one HTML file, run in headless Chromium on Linux, and it makes real elements and measures real boxes with getBoundingClientRect. The left box, the link boxes and the buttons are fixed-size boxes, so the numbers do not depend on fonts. It builds the same row four times, each in frames of six widths. Each row is a grid with three tracks: a left box, a middle container holding five link boxes that cannot wrap, and a right container holding two buttons, with the link boxes centred in the middle container. B has no min-width rule on the middle container or the row. A adds min-width: 0 on the middle container, the pattern in the records. C adds min-width: min-content on the row. D adds both rules to B. For each frame it prints how far the link boxes reach over the left box and over the right container, how far the right container ends past the row's own edge, and whether the frame scrolls. It then repeats the four rows in every frame from 800 px down to 250 px, one pixel at a time, and prints the widest frame in which each variant shows an overlap or an overflow and the narrowest row it produces. Last, it builds C and D again with the min-width: min-content rule on a wrapper around the grid and not on the grid, which is where the merged diff puts it, and counts the frames in which that placement gives the same measurements as the rule on the grid. Every constant is mine and invented: the six frame widths, the box sizes, the gaps, the padding and the five link boxes.

<!doctype html>
<meta charset="utf-8">
<title>nav-grid</title>
<style>
  /* A stand-in, not Northsun code. The left box, link boxes and buttons are fixed-size boxes, so the numbers do not depend on fonts. */
  * { box-sizing: border-box; margin: 0; }
  .frame { overflow-x: auto; margin-bottom: 8px; }
  .row { width: 100%; height: 40px; padding: 0 16px; display: grid; grid-template-columns: auto 1fr auto;
         column-gap: 12px; align-items: center; background: #eee; }
  .left { width: 84px; height: 24px; background: #c00; }
  .mid { display: flex; justify-content: center; gap: 6px; }
  .cell { flex: none; width: 58px; height: 24px; background: #06c; }
  .right { display: flex; gap: 10px; }
  .btn { width: 70px; height: 24px; background: #090; }
  .a .mid, .d .mid { min-width: 0; }               /* A, D: the middle track may shrink below its content */
  .c .row, .d .row { min-width: min-content; }     /* C, D: the row is never narrower than its content */
  .wrap { width: 100%; }
  .cw .wrap, .dw .wrap { min-width: min-content; } /* the same rule on a wrapper around the grid, and not on the grid (C, D) */
  .dw .mid { min-width: 0; }
</style>
<pre id="out"></pre>
<div id="stage"></div>
<script>
  const WIDTHS = [456, 530, 590, 645, 710, 770];
  const VARIANTS = [["a", "A, min-width: 0 on the middle container"],
                    ["b", "B, no min-width rule on the middle container or the row"],
                    ["c", "C, B plus min-width: min-content on the row"],
                    ["d", "D, A plus min-width: min-content on the row"]];
  const box = (cls, n = 1) => Array.from({ length: n }, () => `<div class="${cls}"></div>`).join("");
  const num = (x, w) => String(Math.round(x * 10) / 10).padStart(w);
  const stage = document.getElementById("stage");
  // Build one row in a frame of the given width and measure it. `wrapped` puts a wrapper around the grid.
  function measure(id, width, wrapped = false) {
    const frame = document.createElement("div");
    frame.className = `frame ${wrapped ? id + "w" : id}`;
    frame.style.width = `${width}px`;
    const row = `<div class="row">${box("left")}<div class="mid">${box("cell", 5)}</div><div class="right">${box("btn", 2)}</div></div>`;
    frame.innerHTML = wrapped ? `<div class="wrap">${row}</div>` : row;
    stage.append(frame);
    const r = (el) => el.getBoundingClientRect();
    const rowBox = r(frame.querySelector(".row")), left = r(frame.querySelector(".left"));
    const cells = frame.querySelectorAll(".cell"), right = r(frame.querySelector(".right"));
    const m = { row: rowBox.width,
                overLeft: Math.max(0, left.right - r(cells[0]).left),
                overRight: Math.max(0, r(cells[cells.length - 1]).right - right.left),
                past: Math.max(0, right.right - rowBox.right),
                scrolls: frame.scrollWidth > frame.clientWidth };
    frame.remove();
    return m;
  }
  const lines = ["browser: " + (navigator.userAgent.match(/(Headless)?Chrome\/[\d.]+/) || ["unknown"])[0]];
  for (const [id, label] of VARIANTS) {
    lines.push(label);
    for (const width of WIDTHS) {
      const m = measure(id, width);
      lines.push(`  frame ${num(width, 3)}: row ${num(m.row, 5)}, links over left ${num(m.overLeft, 5)}, links over right ${num(m.overRight, 5)}, ` +
                 `right container past row edge ${num(m.past, 5)}, frame scrolls: ${m.scrolls ? "yes" : "no"}`);
    }
  }
  // The same four rows in every frame from 800 px down to 250 px, one pixel at a time.
  lines.push("Sweep, frames of 800 px down to 250 px in 1 px steps");
  const same = (x, y) => JSON.stringify(x) === JSON.stringify(y);
  const placement = { c: 0, d: 0 };
  let frames = 0;
  for (const [id] of VARIANTS) {
    let last = "none", narrowest = Infinity;
    for (let width = 800; width >= 250; width--) {
      const m = measure(id, width);
      if (id === "a") frames++;
      if (m.overLeft > 0 || m.overRight > 0 || m.past > 0) { if (last === "none") last = width; }
      narrowest = Math.min(narrowest, m.row);
      if ((id === "c" || id === "d") && same(m, measure(id, width, true))) placement[id]++;
    }
    lines.push(`  ${id.toUpperCase()}: widest frame with an overlap or an overflow: ${last}; narrowest row: ${narrowest}`);
  }
  lines.push(`  Frames where the rule on a wrapper around the grid, and not on the grid, matches the rule on the grid: ` +
             `C ${placement.c} of ${frames}, D ${placement.d} of ${frames}`);
  document.getElementById("out").textContent = lines.join("\n");
</script>

Run it with chromium --headless --dump-dom nav-grid.html | sed -n '/<pre id="out">/,/<\/pre>/p' | sed 's/<[^>]*>//g', or open the file in a browser. Output:

browser: HeadlessChrome/141.0.0.0
A, min-width: 0 on the middle container
  frame 456: row   456, links over left    62, links over right    62, right container past row edge     0, frame scrolls: no
  frame 530: row   530, links over left    25, links over right    25, right container past row edge     0, frame scrolls: no
  frame 590: row   590, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 645: row   645, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 710: row   710, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 770: row   770, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
B, no min-width rule on the middle container or the row
  frame 456: row   456, links over left     0, links over right     0, right container past row edge   132, frame scrolls: yes
  frame 530: row   530, links over left     0, links over right     0, right container past row edge    58, frame scrolls: yes
  frame 590: row   590, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 645: row   645, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 710: row   710, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 770: row   770, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
C, B plus min-width: min-content on the row
  frame 456: row   604, links over left     0, links over right     0, right container past row edge     0, frame scrolls: yes
  frame 530: row   604, links over left     0, links over right     0, right container past row edge     0, frame scrolls: yes
  frame 590: row   604, links over left     0, links over right     0, right container past row edge     0, frame scrolls: yes
  frame 645: row   645, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 710: row   710, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 770: row   770, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
D, A plus min-width: min-content on the row
  frame 456: row   456, links over left    62, links over right    62, right container past row edge     0, frame scrolls: no
  frame 530: row   530, links over left    25, links over right    25, right container past row edge     0, frame scrolls: no
  frame 590: row   590, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 645: row   645, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 710: row   710, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
  frame 770: row   770, links over left     0, links over right     0, right container past row edge     0, frame scrolls: no
Sweep, frames of 800 px down to 250 px in 1 px steps
  A: widest frame with an overlap or an overflow: 579; narrowest row: 250
  B: widest frame with an overlap or an overflow: 587; narrowest row: 250
  C: widest frame with an overlap or an overflow: none; narrowest row: 604
  D: widest frame with an overlap or an overflow: 579; narrowest row: 290
  Frames where the rule on a wrapper around the grid, and not on the grid, matches the rule on the grid: C 551 of 551, D 551 of 551

What it shows, with its limits. In this demo the row's content needs 604 px: 84 for the left box, 314 for the five link boxes and their gaps, 150 for the two buttons and their gap, 24 for the two column gaps and 32 for the padding. In the 645, 710 and 770 px frames, all four variants print a row as wide as the frame, with no overlap, no overflow and no scroll. In variant A the middle track shrinks below its link boxes and they reach 62 px over the left box and 62 px over the right container in the 456 px frame, and 25 px over each in the 530 px frame; the row is exactly as wide as its frame, so the frame does not scroll and the overlap is the only symptom. In variant B the track keeps its minimum, nothing overlaps, and the right container ends 132 px and 58 px past the row's own edge in the two narrowest frames; the frame scrolls, but the row's own box ends before its contents do. In variant C, when the frame is narrower than 604 px, the row grows to 604 px, nothing overlaps or passes the edge, and the frame scrolls. The 590 px frame is narrower than the content but inside what the gaps and the padding absorb: A and D print no overlap there because the two 12 px column gaps absorb the first 24 px of shortfall, B prints no overflow because the 16 px right padding absorbs the first 16 px, and C's row already grows to 604 px. The sweep agrees: the widest frame with an overlap (A and D) is 579 px, and the widest with an overflow (B) is 587 px. Variant D prints the same as A in the six frames and in the sweep, except that its narrowest row is 290 px (32 + 84 + 150 + 24), where it stops shrinking and the frame scrolls. My reading: with min-width: 0 on the middle container, this browser's minimum width for the row leaves out the link boxes, so min-width: min-content on the row holds only the other parts. I did not check this against the specification text or other engines, so D is a result for this browser. The last line of the output counts the frames of the sweep in which the rule on a wrapper around the grid gives the same measurements as the rule on the grid. By hand calculation, every printed value for A, B and C equals the arithmetic from the constants above, and the run agrees. These are results of a stand-in with invented sizes, in one browser engine, and I did not test other engines or other alignments of the link boxes.

What this does not show about Northsun

My reading: the mechanism in the demo is of the same kind as the one the records describe.

The demo does not show how Northsun's own bar behaves in any browser. It copies the three-track template and the min-width rules that the description names for the middle container and the bar. Its sizes, gaps, padding, link count and widths were chosen without reference to Northsun's code, and it uses no fonts.

What to look for in your own layouts

My reading, as rules of thumb and not as measurements:

What this still does not prove

FAQ

Why not put min-width: 0 on every grid item?

My reading: it is a common fix when a long word or a long path stretches a column, and it is right for text that may truncate. It can be wrong for items that must stay whole, because it tells the grid they may be squeezed below their content.

The receipts

The Northsun repository and issue tracker are private, so these are descriptions and commit ids for anyone with access to check. The description and the work-log entry carry internal figures, which this post leaves out.