Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-flexbox][css-grid] Unifying grid-auto-flow and flex-flow #11480

Open
fantasai opened this issue Jan 11, 2025 · 6 comments
Open

[css-flexbox][css-grid] Unifying grid-auto-flow and flex-flow #11480

fantasai opened this issue Jan 11, 2025 · 6 comments
Labels
css-flexbox-2 css-grid-3 Masonry Layout tag-tracker Group bringing to attention of the TAG, or tracked by the TAG but not needing response.

Comments

@fantasai
Copy link
Collaborator

fantasai commented Jan 11, 2025

The TAG response to the masonry syntax issue asked us to look into unifying controls for our layout modes, calling out grid-auto-flow and flex-flow (flex-direction + flex-wrap) in particular. Apple looked into this, and we have the following syntax proposal:

Introduce item-flow aliased to both flex-flow and grid-auto-flow and defined as a shorthand for the following properties:

  • item-direction (also aliased as flex-direction)

    • row | column | row-reverse | column-reverse
  • item-wrap (also aliased as flex-wrap)

    • auto | wrap | wrap-reverse | nowrap
    • auto computes to either wrap (for Grid) or nowrap (for Flexbox)
    • nowrap in Grid would mean autoplacement adds implicit tracks instead of wrapping to the next row
  • item-pack

  • item-slack

    • <length-percentage>
    • This is the masonry-slack property. See [css-grid] Decide on a name for masonry-slack #10884.
    • For Flexbox, slack could say at what point you switch from loose packing to cramming:
      • In normal mode, 10px slack would mean “if there’s only 10px overflow on this line when adding the next item, cram it in anyway, as squeezing in an extra 10px is no big deal”.
      • In dense mode, 10px slack would mean “if there’s only 10px empty space left on this line, don’t try to cram in the next item, that’s too much cramming”.

Caveats: This would make flex-flow and grid-auto-flow cascade as a single property, which is a change in behavior and therefore could have some Web-compat impact.

Variations: This is our initial sketch, but there are some variations we’ve considered:

  • We’re unsure if item-slack should be a longhand of item-flow or not: it's often nice to put it in the item-flow shorthand, but it might also make sense for it to cascade independently.
  • We’re open to ideas about the item- prefix. Ideas we’ve come up with so far include item-, box-, items-, and placement-. (We’re drawing the “item” terminology from the specs and from the align-items property.)

Thoughts?

@fantasai fantasai added css-flexbox-2 css-grid-3 Masonry Layout tag-tracker Group bringing to attention of the TAG, or tracked by the TAG but not needing response. labels Jan 11, 2025
@jyasskin
Copy link
Member

I wanted to comment on one piece of this. The rest looks like a great direction to explore, but I'm not enough of a CSS expert to opine that it's definitely the way to go.

Giving a *-direction property values of row and column is ambiguous between "the items are laid out in the row direction" and "the items form visual rows". With flex, these were equivalent; with grid, you always get both rows and columns; but with masonry, they're opposites. I was also surprised to learn that in a vertical writing system (e.g. writing-mode: vertical-rl), "row" means "top to bottom", although it's possible that this is natural to native speakers.

Fundamentally, these say whether to stack items in the block or inline direction. So maybe item-direction should take values of inline, inline-reverse, block, and block-reverse?

@JoshTumath
Copy link

It's great that we're starting to think about solutions to the TAG recommendation for unified layout properties. I think this is a good proposal. And I agree item-* is the best prefix.

Do you see this as part of a family of other item-* properties? Is there any other low hanging fruit of layout properties that could be unified?

For example, I wonder if maybe the syntax for columns and grid-template-columns could be unified as well? columns: 10rem does something a bit similar to grid-template-columns: repeat(auto-fill, 10rem) .

However, I'm thinking about how there are still lots of essential properties needed for each layout system that are unique to each layout system. For example, to create a flex or grid layout, I'm still most likely going to use flex or grid-template-columns.

If I mixed them together in the same layout, I'd probably write something like this:

.my-layout {
  display: flex;
  item-flow: row-reverse wrap;

  > * {
    flex: 1;
  }

  @media (width > 20rem) {
    display: grid;
    grid-template-columns: repeat(auto-fill, 8rem);
  }
}

In the above example, using item-flow has saved me from needing to specify flex-wrap: wrap, flex-direction: row-reverse and grid-auto-flow: row-reverse. But I still won't get the layout I want without flex and grid-template-columns.

@tabatkins
Copy link
Member

(tldr: complaints up top, counter-proposal near the bottom)

I don't think this proposal works. If we were just talking about Flexbox and Grid, this seems pretty reasonable, to the point that I'd likely have supported it if was suggested a few years ago. But in trying to wedge Masonry into the same framework of property and value names, it makes some pretty serious mistakes.

In both Flexbox and Grid, the row/column keyword means "the meaningful layout grouping" (flex lines, or the meaningful direction of grid tracks) matches the keyword: row indicates your flex lines are laid out as rows, or that your grid items are laid out in rows, moving to the next row when one is full and possibly creating new rows if needed. column indicates the opposite. row-reverse and column-reverse both indicate that this layout group is filled in the reverse order, from end to start, rather than start to end as normal.

Also, the wrap/etc keywords in both Flexbox and Grid indicate the direction you wrap to when the row/column (as chosen above) is full; wrap or wrap-reverse tells you where to place the next flex line, or which next grid track to search and where to create new implicit grid tracks when needed.

So far, so good: these two layout modes (Flexbox, and Grid auto-layout) have very similar concepts, used in very similar ways, and the same sorts of terms apply meaningfully across both. There's some tension - dense doesn't have a good meaning in Flexbox (note that the cited issue #3071 is talking about a different behavior than what's proposed here, and neither behavior is really analogous to what Grid is doing †¹), and nowrap doesn't have a good meaning in Grid (I don't understand the behavior described here, but the "no wrap" behavior is already doable by giving the auto-placed item an explicit row or column placement), but either some meaning can be defined for them, or we can define them as error cases and give them a default behavior. Unfortunate, but with some precedent. This is probably a worthwhile cost to pay for the benefit of reusing a single set of properties and values.

These arguments don't apply to Masonry.

@jyasskin brings up the first issue: this proposal defines that row indicates a Masonry whose meaningful layout unit (masonry tracks) is columns. This means the standard Pinterest-style layout would be indicated with item-flow: row collapse, but then pay attention to grid-template-columns, use grid-column for explicit position, etc. This is confusing!

Second, the row-reverse/column-reverse keywords also don't mean the same thing. In Flexbox and Grid, row means each new item will be placed end-ward of the previous item in the same row, while row-reverse means each new item will be startward. But in this proposal for Masonry row-reverse just indicates how to break ties when deciding which column to flow the item into. When placing the first few items in the Masonry this does resemble the usage of row-reverse in the other two, but as soon as the column edges get ragged due to differing item heights, it diverges significantly, only occasionally affecting an item's placement. Most of the time, whether the next item is placed startward or endward of the previous has nothing to do with the keyword, being instead determined by which column is currently shortest. (In the degenerate case where all items are the same size the similarity does persist the whole time, but in that case using Masonry doesn't actually do anything; you could have just used a Grid with identical effect.)

Third, the entire concept of wrapping doesn't apply to Masonry. This proposal defines it as dictating in which direction Masonry fills its tracks, but this exact concept is controlled by item-direction for the other two layout modes! Masonry does not "fill" a layout unit in any meaningful way, and then create new layout units to put subsequent items in, needing a control for where to play the new ones, like Flex and Grid do with their wrap keywords.

Fourth, the additional values still don't have great meanings in Masonry:

  • nowrap doesn't appear to have any possible useful behavior. (I guess it would just put a single item in each track, and generates as many tracks as there are items? That doesn't seem worthwhile to define.).
  • dense has a decently analogous behavior to Grid's definition, but it happens much more rarely, only affecting placement when you're mixing items of varying spans, and only working well when all your tracks are the same size. (In Grid it helps fill in the ends of rows when an item was too wide to fit in the leftover columns and got moved to the next row instead. In Masonry, the placement rules already handle that situation by default. Masonry only gains "holes" to fill if a spanning item covers multiple columns of varying heights. And for perf reasons, it's only allowed to search for holes in the same track as where it would normally be placed, or other tracks with the exact same size.)
  • Masonry adds collapse to indicate the grid is doing masonry rather than normal grid layout, but that doesn't have any meaningful definition for Flexbox.
  • balance has been discussed for Flexbox, but that doesn't have any meaningful definition for Grid or Masonry.

Fifth, while I think applying the 'slack' concept to Flexbox is pretty interesting and worth pursuing †², afaict there's no meaningful definition for Grid.

In conclusion, I think this proposal makes some unacceptable design decisions, which are forced on it due to trying to fit Masonry's layout concepts into the "direction + wrap" concept pair that Flexbox and auto-Grid use. I would oppose this. Instead, Masonry needs its own set of properties to handle the "direction + tie-breaking" concept pair that it actually uses, as I proposed in #11243 (comment).


If we did want to define a single collective set of properties, I think it's possible! A good design just has to recognize that some concepts don't apply to some layout modes, rather than trying to cram all of them into the same set of concepts, like how different layout modes use different subsets of the place-* properties. We'd instead go with:

  • item-direction: row | column | row-reverse | column-reverse: works for all three layout modes, giving the direction of the primary linear layout unit, and which direction to fill that unit in. (So, the standard Pinterest layout is indeed column.)
  • item-wrap: no-wrap | [ wrap | wrap-reverse ] || balance?: only works in Flexbox and Grid. In Grid, nowrap is ignored and treated as wrap, and balance is ignored.
  • item-ties: tie-start | tie-end: only works in Masonry. (Maybe it could apply to a dense Grid or a "dense Flexbox" as I defined it below in †¹; tie-start gives today's dense behavior that always places as early as possible, while tie-end instead places it in the last position preceding the cursor that it fits in, if possible; otherwise in its normal position. That way, items tend to stay close to source order while still filling in gaps.)
  • item-pack: normal | dense: works in Grid and Masonry. (Maybe works in Flexbox, as one of the explored possibilities.)
  • item-slack: <length> | infinite: works in Masonry, and Flexbox as described in the first post of this thread.

If we use Grid-masonry, we'll still need a separate way to indicate that a given Grid is using Masonry instead of Grid layout. If using display:masonry, that's already covered, and leaves us with only masonry-template-tracks, masonry-template-areas, and masonry-track as masonry-specific properties, plus the masonry shorthand that sets some of the item-* props as well.


†¹: What Grid does with dense is always start its search from the top, rather than maintaining a cursor and always proceeding forward. This way, a large item that doesn't fit on one line and gets moved to the next doesn't forever leave an empty space on the preceding line. Translating this to Flexbox, I'd assume it places each item in the first flex line that still has enough space for the item's base size, rather than always putting the item in the last line. It wouldn't change the wrapping behavior or force shrinking in new cases; items would still only shrink if they were the sole item on the line and still too large. I think this would be a useful behavior, as it helps avoid the "one line gets REALLY STRETCHY items because a large item had to wrap to the next line" that today's Flexbox gives.

†²: I really like that the initial value of 0 is just today's Flexbox behavior, and higher values start enabling some slack in line-breaking. However, I don't like that in the proposal for the dense behavior, higher values make it stricter rather than looser. Given that this proposed definition of dense still only ever puts one additional item on a line, I think that *-slack is enough to define it all on its own; item-slack: infinite would give the proposed dense behavior of "always cram one extra item on the line", and smaller values make it stricter. I recognize this is a slightly different constraint -- it's checking how much the next item would overflow by, rather than checking how much space is left in the line -- but I think in practice the two constraints are reasonably interchangable. This then frees up dense to be the behavior suggested in †¹, which is closer to Grid's dense behavior.

@fantasai
Copy link
Collaborator Author

However, I don't like that in the [Flexbox] proposal for the dense behavior, higher values make it stricter rather than looser.

I think you're misreading the proposal. Higher values are looser adherence to dense packing in the proposal (i.e. the larger the slack, the looser packing we allow).

@tabatkins
Copy link
Member

Ah, I'm not misreading, we're just thinking of the condition in opposite ways.

My objection is partly semantic (this is using slack as a negative condition, while the other usages are a positive condition), but the more important one is practical. The point of the slack is to give you some control over the decision of whether to keep the overflowing item on the line (causing shrinkage) or push it to the next line (causing flexing). What's more important for that decision: how much of the item does fit on the line, or how much of the item doesn't fit on the line?

As a practical example, say your flexbox is 1000px wide, and the line is currently 800px filled, so 200px left. The next item is one of two possible elements: either a 250px item, or a 2000px item (lots of text, guaranteed to shrink and internall wrap). What's more important for deciding whether to squeeze either item in or push it to the next line: the fact that there's 200px left on the line, or the fact that one item overflows by 50px (requiring shrinking from 1050 to 1000) and the other overflows by 1800px (requiring shrinking from 2800 to 1000)?

I think it's pretty clear that it's the latter. The 200px is irrelevant for the decision, all that matters is how much shrink the item is going to force everything on the line to do if you cram it in. You're probably okay with the 50px overflow (only 5% over!), but definitely not okay with the 1800px overflow (180% over!).

If you can roughly predict the size of the items, the two numbers are interchangeable: if one is X, the other is size - X, so you can author against either number just fine. But if you can't predict the item sizes, the amount of overflow is, I think, definitely what you want to author against.

So, I think this sort of "dense" behavior is better achieved by just using the proposed "normal flexbox" behavior and setting item-slack: infinite. You can use lower numbers to control how densely you pack.

This then frees up dense to instead act more similar to Grid and Masonry, where it causes the item to try and find an unfilled spot that it'll fit in. I think this would be useful any time someone has an unordered set of data in a multiline flexbox, so one large item wrapping to the next line doesn't leave a ton of free space on the preceding line. And then dense will still work with item-slack - the item finds a line that it can fit in or overflow by the slack amount or less, allowing a little bit of squish.

@tabatkins
Copy link
Member

Ah, here's the positive-condition version of your dense proposal (I rewrote the above comment three times, and of course didn't think of this until after I hit Comment): dense forces the flexbox lines to be completely filled if possible -- aka 0 free space -- by cramming in one more item when needed. item-slack loosens that; flex lines are allowed to have up to the slack amount of free space, but no more.

That said, my practical objection still stands. If you can predict item sizes, then "amount of leftover space on the line" and "amount the item overflows by" are interchangeable and we can use either; if you can't predict item sizes, then it's still more important to control how much overflow is happening than how much free space is left. And dense can then be employed to allow some light rearranging to reduce the free space.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-flexbox-2 css-grid-3 Masonry Layout tag-tracker Group bringing to attention of the TAG, or tracked by the TAG but not needing response.
Projects
None yet
Development

No branches or pull requests

4 participants