CSS Layout API Explained ======================== The CSS Layout API is being developed to improve the extensibility of CSS. Specifically the API is designed to give web developers the ability to write their own layout algorithms in addition to the native algorithms user agents ship with today. For example user agents today currently ship with: - Block Flow Layout - Flexbox Layout However with the CSS Layout API web developers could write their own layouts which implement: - Constraint based layouts - Masonry layouts - Line spacing and snapping Initial Concepts - Writing Modes -------------------------------- This API uses terminology which may be foreign to many web developers initially. Everything in the CSS Layout API is computed in the [logical coordinate system](https://drafts.csswg.org/css-writing-modes-3/#text-flow). This has the primary advantage that when you write your layout using this system it will automatically work for writing modes which are right-to-left (e.g. Arabic or Hebrew), or for writing modes which are vertical (many Asian scripts including Chinese scripts, Japanese and Korean). For a developer who is used to left-to-right text, the way to translate this back into "physical" coordinates is: | Logical | Physical | | -----------:|:-------- | | inlineSize | width | | inlineStart | left | | inlineEnd | right | | blockSize | height | | blockStart | top | | blockEnd | bottom | Getting Started --------------- First you'll need to add a module script into the layout worklet. ```js if ('layoutWorklet' in CSS) { await CSS.layoutWorklet.addModule('my-layout-script.js'); console.log('layout script installed!'); } ``` See the worklets [explainer](../worklets/EXPLAINER.md) for a more involved explanation of worklets. After the promise returned from the `addModule` method resolves the layouts defined in the script will apply to the page. A Centering Layout ------------------ The global script context for the layout worklet has exactly one entry method exposed to developers: `registerLayout`. There are a lot of things going on in the following example so we'll step through them one-by-one below. You should read the code below with its explanatory section. ```js registerLayout('centering', class { async layout(children, edges, constraints, styleMap) { // (1) Determine our (inner) available size. const availableInlineSize = constraints.fixedInlineSize - edges.inline; const availableBlockSize = constraints.fixedBlockSize ? constraints.fixedBlockSize - edges.block : null; let maxChildBlockSize = 0; const childFragments = []; for (let child of children) { // (2) Perform layout upon the child. const fragment = await child.layoutNextFragment({ availableInlineSize, availableBlockSize, }); // Determine the max fragment size so far. maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize); // Position our child fragment. fragment.inlineOffset = edges.inlineStart + (constraints.fixedInlineSize - fragment.inlineSize) / 2; fragment.blockOffset = edges.blockStart + Math.max(0, (constraints.fixedBlockSize - fragment.blockSize) / 2); childFragments.push(fragment); } // (3) Determine our "auto" block size. const autoBlockSize = maxChildBlockSize + edges.block; // (4) Return our fragment. return { autoBlockSize, childFragments, } } }); ``` The `layout` function is your callback into the browsers layout phase in the rendering engine. You are given: - `children`, the list of children boxes you should perform layout upon. - `edges`, the size of *your* borders, scrollbar, and padding in the logical coordinate system. - `constraints`, the constraints which the fragment you produce should meet. - `style`, the _readonly_ style for the current layout. Layout eventually will return a dictionary will what the resulting fragment of that layout should be. The above example would be used in CSS by: ```css .centering { display: layout(centering); } ``` ### Step (1) - Determine our (inner) available size ### The first thing that you'll probably want to do for most layouts is to determine your "inner" size. The `constraints` object passed into the layout function pre-calculates your inline-size (width), and potentially your block-size (height) if there is enough information to do so (e.g. the element has `height: 100px` specified). See [developer.mozilla.org](https://developer.mozilla.org) for an explanation of what [width](https://developer.mozilla.org/en-US/docs/Web/CSS/width) and [height](https://developer.mozilla.org/en-US/docs/Web/CSS/height), etc will resolve to. The `edges` object represents the border, scrollbar, and padding of your element. In order to determine our "inner" size we subtract the `edges.all` from our calculated sizes. For example: ```js const availableInlineSize = constraints.fixedInlineSize - edges.inline; const availableBlockSize = constraints.fixedBlockSize ? constraints.fixedBlockSize - edges.block : null; ``` We keep `availableBlockSize` null if `constraints.fixedBlockSize` wasn't able to be computed. ### Step (2) - Perform layout upon the child ### Performing layout on a child can be done with the `layoutNextFragment` method. E.g. ```js const fragment = await child.layoutNextFragment({ availableInlineSize, availableBlockSize, }); ``` The first argument is the "constraints" which you are giving to the child. They can be: - `availableInlineSize` & `availableBlockSize` - A child fragment will try and "fit" within this given space. - `fixedInlineSize` & `fixedBlockSize` - A child fragment will be "forced" to be this size. - `percentageInlineSize` & `percentageBlockSize` - Percentages will be resolved against this size. As layout may be paused or run on a different thread, the API is asynchronous. The result of performing layout on a child is a `LayoutFragment`. A fragment is read-only except for setting the offset relative to the parent fragment. ```js fragment instanceof LayoutFragment; // true // The resolved size of the fragment. fragment.inlineSize; fragment.blockSize; // We can set the offset relative to the current layout. fragment.inlineOffset = 10; fragment.blockOffset = 20; ``` ### Step (3) - Determine our "auto" block size ### Now that we know how large our biggest child is going to be, we can calculate our "auto" block size. This is the size the element will be if there are no other block-size constraints (e.g. `height: 100px`). In this layout algorithm, we just add the `edges.block` size to the largest child we found: ```js const autoBlockSize = maxChildBlockSize + edges.block; ``` ### Step (4) - Return our fragment ### Finally we return a dictionary which represents the fragment we wish the rendering engine to create for us. E.g. ```js const result = { autoBlockSize, childFragments, }; ``` The important things to note here are that you need to explicitly say which `childFragments` you would like to render. If you give this an empty array you won't render any of your children. Querying Style -------------- While not present in the "centering" example, it is possible to query the style of the element you are performing layout for, and all children. E.g. ```html