Groups column set
The container the categorised list runs in. One column on a phone, two from container 900, three from container 1340. It is a multicolumn box and not a grid, which is the decision worth knowing: the groups flow, so five categories fill two columns by height rather than by count, and a sixth changes nothing about the CSS. Together with the Dashboard head it is the product's whole desktop story.
Anatomy
One box, one gutter, one fragmentation rule. Everything below is drawn by design/system/components/groups-column-set.css and by the category group's own file, through the same entry point a product screen loads, so this page cannot drift from the screens: there is no second copy of the CSS here to drift from.
What you see below depends on how wide this page is, and that is the component rather than a limitation of the stand. The query reads the container declared on body in base.css, so at a narrow window the specimen is correctly one column, from a container of 900 it is two, and from 1340 it is three. The measured numbers are in the technical half.
Streaming (4)
$54.96 a month-
Netflixin 2 days · Aug 3 $17.99 Price changed
-
Disney+in 11 days · Aug 12 $13.99
-
Amazon Primein 19 days · Aug 20 $14.99 Payment failed
-
Huluin 24 days · Aug 25 $7.99
Behaviour at width
From --bp-desktop, 56.25rem, the set becomes multicolumn and counts its own columns. It took two declared steps until 2026-08-13, columns: 2 from a 900 container and columns: 3 from a 1340 one, and it was the only component in the product carrying a third number. That number is gone: see Width.
What settled it was measuring the count rather than arguing about it. This file states its own floor: three columns is the widest the set goes, "a fourth would take a group below the 300px its rows need". Under the declared steps, at a 900px container a column was 269px and stayed under 300 until about 960, so the step standing immediately above the floor was the thing breaking it, for a 60px band, on the etalon screen. And at 1335 a column was 486px, nearly two floors wide, one pixel before snapping back to 310: a count switched by hand grows without limit between the steps and collapses at them.
It now takes columns: 300px, which is a column-width and not a count. Measured after: 1 column to a 962 container, 2 from 963, 3 from 1311, and never narrower than 300 at any width. Founder's decision, 2026-08-13.
It also had a cap of its own for one day, and that cap is deleted. calc(300px * 3 + var(--space-48) * 2) was 996px and it kept "three is the widest" true from the other side. What it produced was a ragged right edge: the shell let every other block of a wide screen take the whole pane while this one stopped at 996, so the two edges disagreed by 144px at a 1440 window, 304 at 1600 and 624 at 1920. That is the white space the founder reported, and it was never this set being too narrow, it was two caps in two files. Now the shell caps everything at --container-wide, 1280, and three columns stay the maximum by arithmetic: a fourth of this file's own 300px floor needs 1344. The set fills its measure, three columns of 395 at a 1600 window.
Software (4)
$53.98 a monthMusic (2)
$22.98 a monthFitness (2)
$24.98 a monthNews (2)
$36.00 a month.groupsthe organism itself, and it is a plaindiv. It reads no colour at all: no fill, no ink, no edge, no radius. Four declarations in the whole filecolumns2from container 900 and3from 1340. Multicolumn and not grid, so the groups FLOW: the browser balances the columns by height and the markup does not say which group goes wherecolumn-gapreads--space-48. The one spacing value this component owns, and the only thing between two columns: there is no rule drawn down the gutterbreak-insideavoid, written onto the group. Fragmentation and not paint, and outside a multicolumn container it does nothing at all, which is exactly why it belongs to the set and not to the group.groupan organism, the Category group: a group head, a list, a hairline between rows. Its own file owns all of that and this one owns none of it.rowa molecule, the Subscription row, fourteen of them here. It is the thing the columns exist to show more of
The set has no head, no count and no total of its own. Every heading you can see belongs to a Group head inside a category group, and the monthly total above the list belongs to the Summary inside the Dashboard head. A container that starts writing its own title has stopped being a container.
Variants and sizes
One axis with three values, and the axis is the container. There is no size, no tone and no emphasis: a column set is a box with a gutter, and everything a person sees inside it belongs to a group or a row. The specimen is not repeated three times below, because there is only one specimen and the reader's own window chooses which value of the axis it is showing. Scroll up, then narrow the window, to move along the axis.
| Columns Chosen by the CONTAINER and never by the page or by how many categories there are. A person with three categories and a person with nine get the same rule, because the thing being fitted is the ROW's own width and not the list's length |
What is declared | Where it stands, and what it measures |
|---|---|---|
One .groups |
nothing | Under container 900 there is no rule at all: the box is a plain block and the groups stack. Measured at container 892: column-count: auto. On a 360px phone the list is one column and each row is the full width, which is the form the whole product was drawn in first |
Two .groups |
columns: 2 column-gap: --space-48 |
From container 900. Measured at container 908: column-count: 2, gutter 48px. On the real coloured Home at a 1440 viewport the set is 893px wide and each column 422.5px, which is within four pixels of the 425px a row gets on a phone-shaped screen. That is the point of the second column rather than a wider row |
Three .groups |
columns: 3 | From container 1340, and the gutter does not change, so it is one declaration. Measured at container 1348: column-count: 3. On the real coloured Home at 1600 the set is 1053px and each column 321.7px; at 1920 it is 1373px and 428.3px |
| Four | - | Forbidden with a reason rather than missing. A fourth column takes a group under the 300px its rows need, and at that width the merchant name starts wrapping into the amount. The rows are the product, so the columns stop before the rows are damaged. If a wider screen ever wants more, what it wants is a wider gutter, not a fourth column |
Multicolumn is the whole decision and it is worth one paragraph. A CSS grid would have to be told how many groups go in each column, so a person with six categories and a person with three would need two answers, and a group would have to be placed rather than allowed to fall. Columns balance themselves by height, so the markup is one list of sections at every width and nothing in the product knows how many categories a person has. The cost is that a group cannot be told to start a new column, and the product has never wanted to.
When to use it
There is one list in this product and this is the container it runs in. J-MAIN reads "I want to see all my recurring charges in one calm view", and the whole argument of the desktop layout is that calm and complete stop fighting each other when the list can run in columns: fourteen subscriptions that need two screens of scrolling on a phone need none at all at 1440.
It is the second half of what the width buys, and the first half is the Dashboard head. Between them they are why Home reads as a desktop product rather than a phone screen stretched wide. Neither is a decoration: the head puts the total and the one thing that needs attention on the same line, and this puts the whole list above the fold. Take both away and a 1440 viewport shows a 780px column with 660px of empty canvas beside it.
Reach for it when a screen holds repeated blocks of the same kind, in an order that does not matter to the eye, and the blocks are short enough to survive being cut off from each other. Do not reach for it when the order is a sequence a person reads in turn: columns break reading order, and a person's eye goes down before it goes across.
The rule, and the anti-rule
Music (2)
$22.98 a monthbreak-inside: avoid keeps a category whole. Without it "Music (2)" and its subtotal end up alone at the bottom of the first column and their two subscriptions start the second, which on the one screen whose job is calm reads as a rendering fault rather than as a layout. It is the only rule this file writes onto its child, and it is fragmentation rather than paint: no colour, no size, no weight, no edge.
When the children are equal things a person chooses between, the component is the Grid (.grid, .grid.roomy and the still unrenamed .plans). A grid places its children in a stated number of tracks and keeps them the same width; this flows them and lets their heights decide. Presets and plan cards in a column set would balance by height and land in an order nobody chose, which for a set of choices is exactly wrong.
The set takes the width the screen gives it, which is why it measures 893px at a 1440 viewport and 1373px at 1920 with no rule of its own about either. Its column count is the only thing that changes, and the rows inside stay between 321px and 428px across that whole range. A width written here would be a screen's measurement living inside a component.
A list where the ORDER carries meaning belongs to the Divided list (.divlist, which was .alerts and .navrows until step 6), which stays one column at every width. Alerts are newest first and settings rows are grouped by subject: flow them into two columns and a person reads the fourth item before the second. Categories have no such order, which is the one property that makes this component legal on Home and illegal on Alerts.
Limits
U17, composition. This block takes order: 3 at the desktop point, and the declaration is the shell's rather than this file's: ordering siblings is the parent's job, and this file refused it first and said so in its header. Since 2026-08-19 this component is also the GUARD on the whole set: .app > .screen:has(> .groups), because the reordering exists to lift the two secondaries above a list running in balanced columns, and a screen with no such list wants its authored order. Full table with sources and how to check: Usage rules.
States
No states, and it is structural rather than an omission. The column set is a container with no fill, no edge and no target of its own: it holds no anchor, no button and no pressed element on any of the 4 grey pages or the 4 coloured ones. A pointer that appears to be over it is over a subscription row, and a pointer in the 48px gutter is over nothing at all. Faking one with a class the product does not carry would document something the product cannot do.
The states inside it are live on their own pages: Subscription row carries hover and focus for the fourteen rows, and Group head has none, for the same reason this one has none.
The technical half
| Reads | Which token | Growing from |
|---|---|---|
| the gutter between columns | --space-48 | primitive, straight: geometry has nothing to override |
| no colour of any kind | none | by decision. The set is a container, and every pixel a person sees inside it belongs to a group, a row or a group head |
| the column count | none: 2 and 3 | A count is not a length and is on no scale. The two container steps it keys off, 900 and 1340, cannot be variables either: a custom property is not valid inside a @container condition, so they are documented and not tokenized |
The gutter folded from 44px to 48px, and it is a real pixel move. kit.css wrote column-gap: 44px, which scales.md does not list among the 27 old values, so the mapping rule was applied as it is written for every other value: nearest step, and a tie goes to the larger. 44 sits exactly between --space-40 and --space-48, so it goes up. Measured consequence at the 893px the product gives the set at a 1440 viewport: each of two columns goes from 424.5px to 422.5px. It belongs in docs/tokens-audit.md under the founder's review of the foundations, the second of the three legal sources of a visual change.
The container queries, measured on this page at both sides of each breakpoint. The container is body, declared in base.css, and this component declares none of its own: a second container between body and these rules would re-point every query inside it at a different box, silently, and a container on the element itself cannot style the element at all, because a container query matches an ancestor.
| Container | Viewport that produces it | column-count | column-gap |
|---|---|---|---|
| 892px | 1175 | auto, one column | normal |
| 908px | 1191 | 2 | 48px |
| 1332px | 1615 | 2 | 48px |
| 1348px | 1631 | 3 | 48px |
The viewport figures are this stand's arithmetic and not a property of the component: on a kit page body carries 268px of left padding from 1100 up, plus a 15px scrollbar, so the container is the viewport minus 283. On a product screen the same queries fire off the screen's own width.
The stand shows it at bleed, and the reason is the axis itself. This is one of the few components that genuinely spans: it is the full width of the screen column and never anything narrower. Measured on the real coloured Home, it renders 893px at a 1440 viewport, 1053px at 1600 and 1373px at 1920. At the default 440px measure it could never show a second column, and at wide's 620 it could not either, so the stand would be documenting a component that has no axis. One honest limit, with its measured consequence: .kit-wrap in _page.css caps a specimen at 900px, so the three-column form can be seen on this page but at 267px a column, where the product's narrowest is 321.7px. At 267 a subscription row wraps its amount below the merchant name, and the product never does that. Read the third column here as evidence that the query fires, not as evidence of how a row looks. The two-column form needs no such caveat: the stand's 900px is within 7px of the 893 the product gives it, so that case is accurate to the pixel.
Not carried, deliberately, and there are two. .app > .screen > .groups { order: 3 } is one of four sibling order declarations that only mean anything together, it names the shell's own column flow, and it belongs to the App shell organism, exactly as trust-block.css decided for order: 4. And .app .group { margin: 0 0 26px } from the 900 block is a no-op after the fold: the base rule is margin: 0 0 24px, and the 8px grid sends 26 and 24 to the same --space-24, so the override became byte identical to the thing it was overriding. It is dropped rather than kept as a no-op, for the reason the button's .compact modifier was dropped: a modifier that modifies nothing costs the next reader a minute. The margin itself lives in category-group.css. Losing 2px there at container 900 is the same fold and the same audit line as the gutter above.
Lives in design/system/components/groups-column-set.css. Stands on 5 grey pages and 5 coloured ones: Home, Home, could not refresh, Home, save focus, Home, loading, and since 2026-08-20 Home, one subscription and Home, a short list, which hold a single group each and therefore fill a single column. Not on Home, nothing yet, which has no list to group.
<div class="groups">
<section class="group">
<div class="group-head"><h2>Streaming (4)</h2><span class="subtotal">$54.96 a month</span></div>
<ul><li><a class="row" href="subscription-detail.html" aria-label="...">...</a></li></ul>
</section>
<section class="group">...</section>
</div>
Five sections and nothing else. The set is never told how many columns to use and a group is never told which column it belongs in.