[css3-grid-layout] Alternative Grid Layout proposal

During the San Diego F2F I was tasked to come up with an alternative proposal for grid layout. This is addressing two issues, the first being preserving the concept of named grid lines (and harmonizing those names with the names in the grid template syntax). I believe the level of indirection between grid lines and grid items is extremely valuable. The second issue is my opinion that the current direction of the grid layout module has become narrowly focused on the task of application UI layout and is not providing the kind of behavior typically expected from traditional typographic layout grids. I feel the current draft is more reminiscent of tables or 2D flexbox than a typographic grid layout paradigm.

To that end, here is my proposal. I believe it's compatible with all the capabilities of the existing grid layout proposal, while also allowing a more flexible usage more in keeping with traditional page layout.


First off, let's start with a definition of terms. There are no rows, columns, or cells. Grid boxes have vertical and horizontal lines, lines can be identified by their ordinal position, or by an identifier, called its 'role'. Multiple roles can be assigned to a single grid line and the same role can be assigned to multiple lines. A rectangular area defined by four grid lines is called a 'field'.

ISSUE: are grid lines based on physical positions or logical positions? i.e. in RTL writing mode do grid lines start from the right? What about vertical writing modes, do horizontal and vertical lines swap? This proposal presumes grid lines are physical only, logical names can be addressed later.

ISSUE: are line roles specified by identifiers or strings? This proposal uses identifiers, this can lead to potential collisions with line positioning keywords. Using strings would eliminate those conflicts and also allow roles and fields to contain spaces or other non-identifier characters.


All grid boxes have four intrinsic lines at their content edges, they have the roles: 'box-left', 'box-top', 'box-right', and 'box-bottom'.

Additional grid lines are defined by the properties: 'grid-lines-horizontal' and 'grid-lines-vertical'. The 'grid-lines-*' properties take a comma separated list of 'line sets', the syntax of a line set is essentially the same as the current definition of the 'grid-definition-*'  properties. Within a line set the position of each line is based on the position of the preceding line. The position of each line set begins at the left or top content edge. This allows the definition of multiple sets of lines used for different purposes. This further enables content based grid line positioning on non-adjacent grid lines.

The grammar looks like this:
grid-lines-vertical: <line-list>
grid-lines-horizontal: <line-list>
<line-list>       => [ <line-set> [, <line-set> ]* ]
<line-set>        => [ <line-role>? <line-group> <line-role>? ]+ 
<line-group>      => <line-minmax> | repeat( <positive-integer> , [ <line-role>? <line-minmax> <line-role>? ]+ ) |
                       repeat-fill( [ <line-role>? <line-minmax> <line-role>? ]+ )
<line-role>       = [ <identifier> ]*
<line-minmax>     => minmax( <line-separation> , <line-separation> ) | auto | <line-separation>
<line-separation> => <length> | <percentage> | <fraction> | min-content | max-content

ISSUE: should negative sizes be allowed between grid lines? I think so.


Grid fields can be defined with the 'grid-fields' property, using the same syntax and rules of the current 'grid-template' property. The use of a 'grid-fileds' property defines the initial horizontal and vertical line sets with line spacing of 'auto' and line roles based on the field name and line position relative to the field, i.e.:
grid-fields: "one one two" "three four four";

is equivalent to:
grid-lines-vertical: one-left three-left auto three-right four-left auto one-right two-left auto two-right four-right;
grid-lines-horizontal: one-top two-top auto one-bottom two-bottom three-top four-top auto three-bottom four-bottom;

In this way there is a direct mapping between fields and lines, any grid lines with the roles of '*-left', '*-top', '*-right', '*-bottom' inherently define a field, and can be referred to by the field name component of the line roles.

When both 'grid-lines-*' properties and 'grid-fields' properties are present, the number of lines and roles assigned by the 'grid-fields' property remain, but the line spacing for the initial line set gets overridden by the spacing in the 'grid-lines-*' properties. Additional line roles defined in the 'grid-lines-*' properties are added to the roles defined by the 'grid-fields' property.

ISSUE: should 'grid-fields' use a list of strings or lists of identifiers separated by a commas? i.e.:
grid-fields: one one two, three four four;
Using identifiers seems more consistent, is it's decided to use strings for field and role names, the horizontal grid lines can still be denoted by commas, i.e.:
grid-flieds: "one" "one" "two", "three" "four" "four";

Some examples of the use of multiple line sets:
Given the grid fields:
+----------+-----+
|   one    | two |
+-------+--+-----+
| three |   four |
+-------+--------+
The vertical grid lines could be specified as:
grid-lines-vertical: one-left auto one-right two-left 1fr, three-left 1fr three-right four-left auto four-right;
allowing both fields 'one' and 'four' to be sized based on their content. This is not possible under the current model.

You can also use line sets to place grid lines absolutely, i.e.:
grid-lines-vertical: 10px, 50px, 150px, 300px;


In the 'grid-lines-*' properties, the 'repeat()' function simply repeats the pattern of lines and roles within it (there being no restriction on the re-use of line roles). Additionally a 'repeat-fill()' function may be useful, it would take a pattern of line positions and roles and would repeat the pattern as many times as necessary to fill the box. There can only be on 'repeat-fill()' within a line set. I'm willing got consider this at risk as there are some interesting issues with 'repeat-fill()' and auto line positioning vs auto grid container sizing. Perhaps a very restricted use for level 1?


Now here's where I take a significant departure from the current model, placing items within the grid. Rather than have the notion of grid columns, rows, and spans, we simply use absolute or relative positioning (and possibly 'page' and 'fixed'), and existing positioning properties with a few convenient shorthands added for brevity.

In this model, all containing blocks are grid containers. We can either define the 'grid-lines-*' properties automatically turn their box into a containing block, or we can add an explicit 'containing-block' property with the values of 'auto' or 'always' (which I really want for other reasons as well). 


As an example, given a grid container:

#grid {
    grid-lines-vertical: 50px images-left 50px text-left 50px images-right 250px text-right 50px;
}

and the following markup:
<div id='grid'>
  <p>Some text...
  <img src='foo.gif'>
   more text...</p>
</div>

You can place the text and images with the following CSS:
p {
  position: absolute;
  left: grid-line(text-left);
  right: grid-line(text-right);
}
img {
  position: absolute;
  left: grid-line(image-left);
  right: grid-line(image-right);
}

The 'grid-line(<grid-line>)' function takes a single identifier, followed by an optional integer, or a single integer. When an identifier is followed by an integer, it means use the nth grid line with that role. If a line of the given role cannot be located, the 'grid-line()' function comutes to 'auto'. If a single integer is used, it is specifying the ordinal number of the grid line to use. Ordinal values begin counting at 0 and count the grid lines as defined in the respective 'grid-lines-*' property, negative numbers count from the end. If the ordinal value exceeds the number of defined grid lines, additional grid lines are appended with a position of 'auto'.

The 'grid-line()' function can be used for the value of 'left', 'top', 'right', 'bottom', 'width', and 'height'. When used for 'width' (or 'height'), if both 'left' and 'right' are not 'auto', then width is treated as 'auto'. If 'right' is 'auto', then the right side is located at the grid line specified in 'width' as though relatively positioned from the left side working rightwards. If 'right' is not 'auto' and 'left' is 'auto', then the left side is located by the grid line specified relative to the right side working leftwards. 'height' works similarly with the 'top' and 'bottom' sides.

You can mix and match left/top/right/bottom values with grid-line() functions and any other allowable values for those properties. In the preceding example, 'top' and 'bottom' are left to their default 'auto' values. This has the effect of snapping the left and right edges of the <p> and <img> elements to their assigned grid lines while leaving their vertical positions where they would be if statically positioned.

Relative positioning works by locating the appropriate grid line relative to the position the box would have if it were statically positioned. For example, if a box is relatively positioned and the 'left' property is set to 'grid-line(column-left)', then the left edge gets positioned at the first grid line to the right of the element's static position with the role of 'column-left'. If the grid line is specified with an ordinal value, then the nth grid line from the static position is used. Negative numbers are allowed.


The following additional functions are defined for the values of the 'position' property:
grid-lines(<grid-line>[4])
grid-lines-relative(<grid-line>[4])
grid-field(<identifier>)
grid-field-relative(<identifier>)

ISSUE: should the order of lines in 'grid-lines()' be TRBL or LTBR? TRBL is used in this proposal.
ISSUE: should 'grid-lines()' be allowed to take less than 4 grid lines? If so, what are the unspecified lines defined as?

The 'grid-*()' functions when used in the position property act as shorthands for the 'position', 'left', 'top', 'right' 'bottom', 'width', and 'height' properties. For example, the following are equivalent:

position: grid-field(foo);
position: grid-lines(foo-top foo-right foo-bottom foo-left);
position: absolute; left: grid-line(foo-left); top: grid-line(foo-top); right: grid-line(foo-right); bottom: grid-line(foo-bottom); width: auto; height: auto;

The 'grid-*-relative()' versions of the functions set the 'position' to 'relative'.

For the 'grid-field()' functions, if lines with the roles based on the field name are not present, those edges compute to 'auto'. (Alternatively, 'width' and/or 'height' could compute to 'grid-line(1)', meaning the next grid line).


Using positioning to place grid items, I believe, results in the same grid line positioning algorithm as currently specified in the grid layout module. The only added complexity is that, when placing the grid lines, the grid container must traverse its positioned descendants to find boxes positioned on pairs of grid lines in order to determine which content is used to compute the line position (for those grid lines whose positions are based on content).


One behavior of the current grid layout proposal not addressed so far is the automatic sizing of the grid container (since positioned children do not normally impact the size of the containing block). I have three proposals for how to achieve the current behavior, in personal preference order:
1) add a new value to 'width' and 'height', call it, 'all-content'. This behaves similarly to 'fit-content' except that is also takes into account positioned children.
2) keep 'display: grid' (which is otherwise no longer necessary). This would behave like a normal containing block with the additional behavior that 'width' and 'height' values of 'auto' account for grid positioned children.
3) define 'auto' for grid containers to simply take grid positioned children into account.


The other behavior of the current grid layout proposal not addressed so far is the automatic grid item placement algorithm. I propose that this behavior can be achieved using the yet-to-be defined 'collisions' behavior that was also discussed at the San Diego F2F. I believe the collisions behavior should have a property defining the direction to search for alternative placement of positioned items when they collide with other positioned items. When using grid positioning, subsequent grid lines can be searched for by role, if specified, or physical position.


Currently, the 'justify-self' and 'align-self' properties are defined to work on grid items. I propose that these be changed to apply to positioned items when the relevant edges compute to 'auto'.


An additional behavior that I considered would be the ability to center items on grid lines. One thought I had there is to add the properties 'center' and 'middle', which would have the default value of 'auto'. When set for positioned items to something other than 'auto' and 'left' and 'right' (or 'top' and 'bottom') are 'auto', these would place the box's center (or middle) at the specified location (which could be any location, not necessarily a grid line). This also would eliminate the need for the 'center' value of 'position' in css3-positioning.


In addition, we need to specify the behavior of grid lines when the grid container is fragmented. I propose the following property:
grid-break: slice | clone;
which behaves similarly to 'box-decoration-break'. 'clone' would repeat all the grid lines in each fragment starting at the beginning. When used on the viewport, this can easily create page grids.


Feedback welcome,

Peter

--------

As a sanity check, I recreated all the examples from the current grid layout proposal in terms of this proposal. They are included below:


Example 1
<style type="text/css">
    #grid { 
        width: 100%;
        height: 100%;

        /* Two columns: the first sized to content, the second receives the remaining space,   */
        /* but is never smaller than the minimum size of the board or the game controls, which */
        /* occupy this column. */
        grid-lines-vertical: auto minmax(min-content, 1fr);

        /* Three rows: the first and last sized to content, the middle row receives the        */
        /* remaining space, but is never smaller than the minimum height of the board or stats */
        /* areas. */
        grid-lines-horizontal: auto minmax(min-content, 1fr) auto
    }

    /* Each part of the game is positioned between grid lines by referencing the index of */
    /* the line in the order top right bottom left, which establishes bounds for the part. */
    #title    { position: grid-lines(0 1 1 0) }
    #score    { position: grid-lines(2 1 3 0) }
    #stats    { position: grid-lines(1 1 2 0); justify-self: start }
    #board    { position: grid-lines(0 2 2 1) }
    #controls { position: grid-lines(2 2 3 1); align-self: center }
</style>

<div id="grid">
    <div id="title">Game Title</div>
    <div id="score">Score</div>
    <div id="stats">Stats</div>
    <div id="board">Board</div>
    <div id="controls">Controls</div>
</div>


Example 2
<style type="text/css">
    #grid { 
        width: 100%;
        height: 100%;
    }
    @media (orientation: portrait) {
        #grid { 
            /* The lines and fields of the grid are defined visually using the         */
            /* grid-fields property.  Each string is a row, and each word a field.     */
            /* The number of vertical lines is determined by the number of words + 1.  */
            /* The number of horizontal lines is determined by the number of           */
            /* strings + 1. Note the number of words in each string must be identical. */
            grid-fields: "title stats"
                         "score stats"
                         "board board"
                         "ctrls ctrls";
            
            /* Grid lines created with the template property can be assigned a sizing */
            /* function with the grid-lines-vertical and grid-lines-horizontal properties. */
            grid-lines-vertical: auto minmax(min-content, 1fr); 
            grid-lines-horizontal: auto auto minmax(min-content, 1fr) auto
        }
    }

    @media (orientation: landscape) {
        #grid { 
            /* Again the grid-field defines fields of the same name, but this time */
            /* positioned differently to better suit a landscape orientation.      */
            grid-fields: "title board"
                         "stats board"
                         "score ctrls";
            
            grid-lines-vertical: auto minmax(min-content, 1fr); 
            grid-lines-horizontal: auto minmax(min-content, 1fr) auto
        }
    }

    /* The grid-area property places a grid item into named region (area) of the grid. */
    #title    { position: grid-field(title) }
    #score    { position: grid-field(score) }
    #stats    { position: grid-field(stats) }
    #board    { position: grid-field(board) }
    #controls { position: grid-field(ctrls) }
</style>

<div id="grid">
    <div id="title">Game Title</div>
    <div id="score">Score</div>
    <div id="stats">Stats</div>
    <div id="board">Board</div>
    <div id="controls">Controls</div>
</div>



Example 3
<style type="text/css">
    #grid { 
        /* The grid-lines-vertical and horizontal properties also support assigning a role to grid lines */
        /* which can then be used to position grid items.  The line roles are assigned on    */
        /* either side of a line placement function where the line would logically exist. */
        grid-lines-vertical:      
            start        auto 
            track-start  0.5fr 
            thumb-start  auto 
            fill-split   auto 
            thumb-end    0.5fr 
            track-end    auto
            end;
    }

    /* grid-column and grid-row accept a starting and optional ending line. */
    /* Below the lines are referred to by role. Beyond any semantic advantage, the names  */
    /* also allow the author to avoid renumbering the grid-row-position and      */
    /* column properties of the grid items.  This is similar to the concept demonstrated in the */
    /* prior example with the grid-template property during orientation changes, but    */
    /* grid lines can also work with layered grid items that have overlapping areas of    */
    /* different shapes like the thumb and track parts  in this example. */
    #lower-label { position: absolute; left: grid-line(start); right: grid-line(track-start); align-self: center }
    #track       { position: absolute; left: grid-line(track-start); right: grid-line(track-end); align-self: center }
    #upper-label { position: absolute: left: grid-line(track-end); right: grid-line(end); align-self: center }
    
    /* Fill parts are drawn above the track so set z-index to 5. */
    #lower-fill  { position: absolute; left: grid-line(track-start); right: grid-line(fill-split); align-self: center; z-index: 5 }
    #upper-fill  { position: absolute: left: grid-line(fill-split); right: grid-line(track-end); align-self: center; z-index: 5 }
    
    /* Thumb is the topmost part; assign it the highest z-index value. */
    #thumb       { position: absolute; left: grid-line(thumb-start); right: grid-line(thumb-end); z-index: 10 }
</style>

<div id="grid">
    <div id="lower-label">Lower Label</div>
    <div id="upper-label">Upper Label</div>
    <div id="track">Track</div>
    <div id="lower-fill">Lower Fill</div>
    <div id="upper-fill">Upper Fill</div>
    <div id="thumb">Thumb</div>
</div>




Example 4 - actually n/a
<style type="text/css">
    #grid { 
        grid-lines-vertical: 150px 1fr; /* two columns */
        grid-lines-horiztonal: 50px 1fr 50px /* three rows  */
    }
</style>



Example 5
<style type="text/css">
    #grid { 
        grid-lines-vertical: 150px 1fr;
        grid-lines-horizontal: 50px 1fr 50px
    }

    #item1 {  position: grid-lines(0 2 3 1) }
</style>



Example 6
<style type="text/css">
    /* equivalent layout to the prior example, but using line roles */
    #grid { 
        grid-lines-vertical: 150px item1-left 1fr item1-right;
        grid-lines-horiztonal: item1-top 50px 1fr 50px item1-bottom
    }

    #item1 { 
	position: grid-lines(item1-top item1-right item1-bottom item1-left);
	/* alternatively */
	position: grid-field(item1);
    }
</style>




Example 7
<style type="text/css">
    /* using the grid-field syntax */
    #grid  { 
        grid-fields: ". a"
                     "b a"
                     ". a";
        grid-lines-vertical: 150px 1fr;
        grid-lines-horizontal: 50px 1fr 50px
    }

    #item1 { position: grid-field(a) }
    #item2 { position: grid-field(b) }
    #item3 { position: grid-field(b) }

    /* Align items 2 and 3 at different points in the Grid field "b".  */
    /* The grid-field() position sets all four edges of the box, */
    /* individual sides can be overridden to align items. */
    #item2 { bottom: auto }
    #item3 { left: auto; top: auto }</style>




Example 8
<style type="text/css">
    #grid { 
        grid-lines-vertical: 150px 1fr;
        grid-lines-horizontal: 50px 1fr 50px
    }
</style>



Example 9
<style type="text/css">
    #grid {
        grid-lines-vertical: first nav 150px main 1fr last;
        grid-lines-horizontal: first header 50px main 1fr footer 50px last;
    }
</style>



Example 10
<style type="text/css">
    #grid {
        grid-lines-vertical: 10px content 250px 10px content 250px 10px content 250px 10px content 250px 10px;
    }

    /* Equivalent definition. */
    #grid {
        grid-lines-vertical: 10px repeat(4, content 250px 10px);
    }
</style>




Example 11
div { grid-lines-vertical: 100px 1fr max-content minmax(min-content, 1fr) }



Example 12
    /* examples of valid line definitions */
    grid-lines-horizontal: 1fr minmax(min-content, 1fr);
    grid-lines-horizontal: 10px repeat(2, 1fr auto minmax(30%, 1fr));
    grid-lines-horizontal: (10px);
    grid-lines-horizontal: calc(4em - 5px)




Example 13
<style type="text/css">
    #grid {
        width: 500px;
        grid-lines-vertical: 
            a     auto 
            b     minmax(min-content, 1fr) 
            b c d repeat(2, e 40px) 
                  repeat(5, auto);
    }
</style>
<div id="grid">
    <div style="position: absolute; left: grid-line(a); right: grid-line(b); width:50px"></div>
    <div style="position: absolute; left: grid-line(8); width:50px"></div>
</div>
<script type="text/javascript">
    // Returns 'a 50px b 320px b c d repeat(2, e 40px) repeat(4, 0px) 50px'.
    var gridElement = document.getElementById("grid");
    window.getComputedStyle(gridElement, null).getPropertyValue("grid-lines-vertical");
</script>




Example 14
<style type="text/css">
    #grid {
        grid-fields: "head head"
                     "nav  main"
                     "foot ."
    }
    #grid > a {
        position: grid-field(nav);
    }
</style>




Example 15  - n/a as no positioned children of a grid container simply use static positioning


Example 16
<style type="text/css">
#item {
    /* the following two property definitions are equivalent */
    /* both place the item between the first and third line */
    /* which is covering the first and second row of the Grid */
    top: grid-line(0); bottom: grid-line(2);
    top: grid-line(0); height: grid-line(2);
}
</style>



Example 17 & 18 - n/a shorthands no longer present



Example 19
<style type="text/css">
    #grid {
        display: grid;
        grid-lines-horizontal: header auto main 1fr footer auto;
    }
 
    #header { position: absolute; top: grid-line(header); bottom: grid-line(main) }
    #main   { position: absolute; top: grid-line(main); bottom: grid-line(footer) }
    #footer { position: absolute; top: grid-line(footer); bottom: grid-line(box-bottom) }

    /* Equivalent to the above using grid line numbers instead of names. */
    #header { position: absolute; top: grid-line(0); bottom: grid-line(1) }
    #main   { position: absolute; top: grid-line(1); bottom: grid-line(2) }
    #footer { position: absolute; top: grid-line(2); bottom: grid-line(3) }
</style>



Example 20
<style type="text/css">
    #grid { grid-lines-vertical: 20px; grid-lines-horizontal: 20px }
    #A { position: absolute; left: grid-line(0); top: grid-line(0) }
    #B { position: absolute; left: grid-line(5); top: grid-line(0); height: grid-line(2) }
    #C { position: absolute; left: grid-line(0); top: grid-line(1); width: grid-line(2) }
</style>

<div id="grid">
    <div id="A">A</div>
    <div id="B">B</div>
    <div id="C">C</div>
</div>



Example 21 - depends on to be defined collisions property
<style type="text/css">
    form {
	display: block;
        grid-lines-vertical: labels auto controls auto oversized auto;
        grid-lines-horizontal: repeat-fill(row auto) buttons auto
    }
    form > input, form > select {
        /* Place all controls in the "controls" column and automatically find the */
        /* next available row. */
        position: absolute;
        left: grid-line(controls);
        top: grid-line(row);
        collision: down;
    }
    form > label {
        /* Place all labels in the "labels" column and automatically find the next
        /* available row. */
        position: absolute;
        right: grid-line(controls);
        top: grid-line(row);
        collision: down;
    }
    
    #department {
        /* Auto place this item in the "oversized" column in the first row where an area that  */
        /* spans three rows won't overlap other explicitly placed items or areas or any items */
        /* automatically placed prior to this area. */
        position: absolute;
        left: grid-line(oversized);
        top: grid-line(row);
        collision: down;
        height: grid-line(3);
    }

    /* Place all the buttons of the form in the explicitly defined grid area. */
    #buttons {
        position: absolute;
        top: grid-line(buttons);
        right: grid-line(box-right);
    }
</style>
<form action="#">
    <label for="firstname">First name:</label>
    <input type="text" id="firstname" name="firstname" />
    <label for="lastname">Last name:</label>
    <input type="text" id="lastname" name="lastname" />
    <label for="address">Address:</label>
    <input type="text" id="address" name="address" />
    <label for="address2">Address 2:</label>
    <input type="text" id="address2" name="address2" />
    <label for="city">City:</label>
    <input type="text" id="city" name="city" />
    <label for="state">State:</label>
    <select type="text" id="state" name="state">
        <option value="WA">Washington</option>
    </select>
    <label for="zip">Zip:</label>
    <input type="text" id="zip" name="zip" />
    
    <div id="department">
        <label for="department">Department:</label>
        <select id="department" name="department" multiple>
            <option value="finance">Finance</option>
            <option value="humanresources">Human Resources</option>
            <option value="marketing">Marketing</option>
        </select>
    </div>

   <div id="buttons">
       <button id="cancel">Cancel</button>
       <button id="back">Back</button> 
       <button id="next">Next</button>
   </div>
</form>




Example 22
<style type="text/css">
    #grid {grid-lines-vertical: 1fr 1fr; grid-lines-horizontal: 1fr 1fr }
    #A { position: absolute; left: grid-line(0); bottom: grid-line(2); width: grid-line(2) }
    #B { position: absolute; left: grid-line(0); top: grid-line(0); z-index: 10 }
    #C { position: absolute; left: grid-line(1); top: grid-line(0); margin-left: -20px }
    #D { position: absolute; right: grid-line(2); top: grid-line(1) }
    #E { position: absolute; z-index: 5;
         justify-self: center; align-self: center 
    }
</style>

<div id="grid">
    <div id="A">A</div>
    <div id="B">B</div>
    <div id="C">C</div>
    <div id="D">D</div>
    <div id="E">E</div>
</div>

Received on Tuesday, 4 September 2012 01:35:06 UTC