D
P
0
← All articles Baca dalam Bahasa Indonesia

CSS Layout, Overflow & Cascade

Grid `1fr` Columns Not Equal Width, 272px vs 27px? `1fr` Is `minmax(auto, 1fr)` and an Unbreakable Title Stretches It

· · 11 min read
Grid `1fr` Columns Not Equal Width, 272px vs 27px? `1fr` Is `minmax(auto, 1fr)` and an Unbreakable Title Stretches It

On paper, repeat(7, 1fr) means seven columns of equal width. In the monthly calendar I rebuilt for a client's web app, the same declaration produced four columns between 271 and 275 pixels wide and three columns between 26 and 27 pixels wide.

The app is React 18 built on Create React App through react-scripts 5.0.1, with its styles written in styled-components 5. I shipped the new calendar as a feature branch on 8 September 2026, the same evening the client asked for it. That branch was recorded as merging cleanly into the client's development branch, and the production build passed. The month view draws 42 cells, including days from the previous and next months, and each cell shows at most three event chips. The rest collapse into a "+N more" button.

The report came from the client's backend developer, against the branch that had already been pushed: some calendar cells were large and the rest were small. That finding was correct.

Seven columns, two sizes

Measured before the fix, the seven column widths were 272, 275, 275, 271, 27, 27, and 26 pixels. The widest column is more than ten times the narrowest, since 275 divided by 26 is about 10.6.

The heights were uneven too. A 42-cell grid with seven columns means six week rows, and those rows split into two sizes, 88 and 95 pixels. According to the fix's commit message, a cell held only a min-height, so a week with events stood taller than an empty one, 95 against 88.

The column numbers say one more thing. Before the fix, the seven columns add up to 272 + 275 + 275 + 271 + 27 + 27 + 26 = 1,173 pixels. After the fix, on the same data, each of the seven is 115 pixels, so 7 × 115 = 805 pixels. The four wide columns were not simply taking space from the three narrow ones, because the row of seven columns as a whole was 368 pixels wider. The width of the grid's container is not in my notes, so I am leaving that difference as arithmetic rather than a claim about how far the grid overflowed.

My notes describe the unevenness as two separate causes with one shared root: a grid item or a flex item refusing to shrink. The grid one sits in the column tracks. The flex one sits inside the event chip.

1fr has a floor, and the floor is auto

Before the fix, the month grid and its weekday header both used repeat(7, 1fr), and the month cell had min-height: 88px. Inside the cell, an event chip is a flex button with a separate span for its title. Here is a version simplified to plain CSS, with the class names neutralised, since the original is written in styled-components:

/* simplified from styled-components, class names neutralised */
.days-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
}
 
/* the weekday header uses the same column declaration */
.weekdays {
  grid-template-columns: repeat(7, 1fr);
}
 
.day {
  min-height: 88px;
}
 
.chip {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
}
 
.chip .label {
  overflow: hidden;
  text-overflow: ellipsis;
}

The part that is easy to miss is that 1fr does not stand alone. Outside minmax(), a flexible value like 1fr carries an automatic minimum, so repeat(7, 1fr) is really repeat(7, minmax(auto, 1fr)). An auto minimum means the column may not be narrower than the minimum size of the items inside it. Grid items default to min-width: auto, and as long as their overflow is visible, that minimum is the min-content width of their contents, the narrowest the content can get without overflowing. For text, that is the width of the longest piece that cannot be broken.

The calendar's data had pieces like that. As one of the commit messages records, meetings whose organiser never typed a title arrive named by their join link. Several of those video call link titles run to roughly 36 characters with no break opportunity, inside a chip that sets white-space: nowrap anyway. One title like that is enough to stretch its column, and because the seven columns share the same space, the others are the ones that get squeezed.

Once the combined minimums of the seven columns exceed the available space, 1fr has no leftover space to hand out, and each column stops at its own minimum even if that makes the grid wider than its container. The comment on the weekday header after the fix puts the mechanism more briefly: a plain 1fr still refuses to shrink below the widest thing inside the column, so one long, unbreakable meeting title drags its column wide and squeezes the other six.

The ellipsis on the wrong box

The flex part lives inside the chip. The event chip is a display: flex button whose own box sets white-space: nowrap, overflow: hidden, and text-overflow: ellipsis, both before and after the fix. That last declaration does nothing there. text-overflow applies only to block containers, and a flex container is not one, since its contents are laid out as flex items. A mock in the client's mockup pack makes the same mistake by putting text-overflow: ellipsis straight on its chip's flex box, and the conclusion my notes drew for that mock applies here as well: the title needs its own element.

In my build the title already had its own element, a span that before the fix held only overflow: hidden and text-overflow: ellipsis. My notes at the time put the blame on that span. It carried the ellipsis without min-width: 0, and since a flex child will not shrink below its content without that declaration, the chip grew instead of truncating. The same notes say this single missing declaration is what let the whole column inflate.

That part is weaker than it sounds. The rule that a flex item will not shrink below its content applies to items whose overflow is visible. The flexbox spec gives an item that is a scroll container an automatic minimum size of zero, and overflow: hidden already makes an item a scroll container. That span had overflow: hidden from before the fix. The measurements I have also compare only the state before and after the whole fix, so no number can separate this span's share from the share of the changes at the grid level.

A zero floor at every level

The fix was done on its own branch, two commits on top of the feature branch. The dimensions commit changed a single calendar style file, 20 lines added and 2 removed, and is recorded as done on 9 September 2026. Below are only the relevant lines, and the text of the two comments inside is quoted verbatim from the code:

.days-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  /* Every week the same height, so the grid stays a grid. Without this
     a week holding three events stands taller than an empty one. */
  grid-auto-rows: 1fr;
}
 
.weekdays {
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
 
.day {
  /* A grid item defaults to min-width:auto, which is its content width.
     Without this the cell cannot shrink and neither can its column. */
  min-width: 0;
  overflow: hidden;
}
 
.chip {
  width: 100%;
  min-width: 0;
  max-width: 100%;
}
 
.chip .label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

The month grid and the weekday header both moved to repeat(7, minmax(0, 1fr)). The header has to follow, because if its column declaration differs from the grid's, the day labels stop lining up with the columns underneath. The cell got min-width: 0 and overflow: hidden. The chip box got min-width: 0 and max-width: 100% next to the width: 100% it already had. The title span got min-width: 0 and white-space: nowrap, with a code comment saying that min-width: 0 is what actually makes the ellipsis work.

Some of these declarations cover for each other. Under the grid spec, an item's automatic minimum size is taken from its content only if the item is not a scroll container and sits in a track whose minimum is auto. overflow: hidden on the cell removes the first condition, and minmax(0, 1fr) removes the second. Everything went into one commit, so the measurement holds for the combination: on the same data, all seven columns now measure 115 pixels each.

Rows are handled by grid-auto-rows: 1fr. In a grid whose height follows its content, every 1fr row gets the same height, the height of the row with the tallest content. The measurement matches that: rows that were 88 and 95 pixels are now a flat 95.

The client's mockup pack turns out to have the same min-height, and according to my notes the same defect shows up in its own drawing. The pack also defines a class that its mock never uses. The fix's commit message calls that class the answer the pack was reaching for: cap the cell and say how many are hidden. My build had already done that since the rebuild commit on 8 September, one day before the grid fix, as three chips per cell and a "+N more" button for the rest.

Once truncated, the title just says "https:..."

Once truncation worked, every chip whose title was a link read "https:..." and said nothing. The columns were even, but the chips had stopped meaning anything.

The decision was that a URL-shaped title now prints only its path, titles that are not URLs are left alone, and the tooltip keeps the original title. The commit subject states the intent for join links, print the meeting code when a title is just a join link. Here is the function, with the function, component, and class names neutralised:

// function, component, and class names neutralised
function shortTitle(name) {
  const raw = (name || '').trim()
  if (!raw) return 'Untitled'
  if (!/^https?:\/\//i.test(raw)) return raw
  try {
    const u = new URL(raw)
    const tail = u.pathname.replace(/^\/+|\/+$/g, '')
    return tail || u.hostname
  } catch {
    return raw
  }
}
 
// the tooltip holds the original title, the span holds the short one
<button className="chip" title={`${formatTime(event)} ${event.name}`}>
  <span className="time">{formatTime(event)}</span>
  <span className="label">{shortTitle(event.name)}</span>
</button>

An empty name becomes "Untitled". A name that does not start with http:// or https:// passes through unchanged. For a URL, what comes back is the path with slashes trimmed from both ends, or the hostname when the path is empty, and a string that fails to parse as a URL comes back whole. The chip button's title attribute still uses the original name, so the tooltip keeps the full link while the title span gets the short version. This label went in as a separate commit, directly on top of the dimensions fix.

From the fix branch to the client's branch

On 9 September, the calendar branches, including the grid fix branch, were merged into my working branch and then deleted. Five days later, on 14 September, their content reached the client's development branch through a squash merge recorded under the client's backend developer's name. Because of the squash, the original commit SHAs are not ancestors of the client's branch, so what needs checking is the code's content, not its commit ancestry. The content is there: on the client's development branch, the month grid still uses repeat(7, minmax(0, 1fr)) and grid-auto-rows: 1fr.

The limits need stating too. The 115 and 95 pixel measurements were taken on the fix branch on 9 September. My notes do not mention a re-measurement after that squash, nor after a production deploy.

The floor nobody wrote

Neither auto in this case was ever typed into the stylesheet by anyone. The first is hidden inside 1fr, and the second sits in the default min-width of grid items and flex items. min-width: 0 on a flex item is a fairly well known remedy. What makes this case different is that the same floor also sits in the grid track, and there it widens an entire column, not just one element.

If grid columns must stay equal whatever they contain, write the floor out explicitly with minmax(0, 1fr) and let the cell's content adapt to its column, not the other way around. If text inside a flex box has to truncate with an ellipsis, put text-overflow on a child element rather than on the flex box, make sure that child is allowed to shrink through min-width: 0 or an overflow other than visible, and make sure the box above it has a real width limit.

After the text truncates, read what is left. In this calendar, truncation that worked exactly as intended produced a row of "https:..." that told nobody anything, and the way out was in the label being printed, not in the CSS.