Item lists
The smallest theme hook anybody has to style, and the one whose markup differs most between core and this theme: `mantra_starter` keeps the `div.item-list` wrapper it inherited from `starterkit_theme` and styles everything through it, where core emits no wrapper at all. The first half of the page is the hook -- what each of its seven variables does to the markup, and the four shapes core puts a list into. The second is the stylesheet: the grid wrapper, `break-inside: avoid`, `--item-list-line-height` and the adjacent-sibling margin, each rendered twice, with the declaration and without it. Every item is made up and nothing is saved.
What the theme emits
Three templates are in play and they do not agree on the markup. Core's own -- `core/modules/system/templates/item-list.html.twig` -- emits no wrapper: the `h3` and the list are siblings in whatever happens to contain them, and there is nothing on the page that is the list *and* its label. `starterkit_theme`, which `mantra_starter` was generated from, wraps both in a `div.item-list` and puts `item-list--` followed by the style name on that wrapper. `mantra_starter` keeps the wrapper and names the two elements inside it, `item-list__label` on the heading and `item-list__list` on the list. Neither of those two classes is styled by anything, in this theme or in core; they exist so that a rule has somewhere to land that is not an element selector, because `h3` and `ul` inside a wrapper are shared with everything else on the page. Everything the theme *does* style, it styles through `.item-list` on the wrapper -- which is why the wrapper matters here in a way it does not in core, and why every declaration in the second half of this page is written against it rather than against the list.
- Article
- Basic page
- Task
<div class="item-list">
<ul class="item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
</ul>
</div>
- .item-list
- --item-list-line-height: 1; line-height: var(--item-list-line-height); break-inside: avoid; display: grid
- .item-list + .item-list
- margin-top: calc(var(--spacing) * 4)
The title, and the empty message
`#title` becomes an `h3` and takes whatever it is given -- a string, a `0`, or a whole render array; the template tests `is not empty`, which in Twig is false for `0`, so a list titled zero keeps its heading. `#empty` is the one that surprises: it renders *instead of* the list rather than inside it, so what comes back is the wrapper, the heading if there is one, and a bare text node where the list would have been. There is no `li`, no `ul`, and nothing carrying `item-list__list` -- a stylesheet that reaches the empty message through the list will never find it. With neither items nor an empty message the template emits nothing at all, not an empty wrapper: `{% if items or empty %}` guards the whole body, which is what lets a caller hand an item list a possibly-empty array without guarding it themselves.
- Article
- Basic page
- Task
<div class="item-list">
<div class="item-list__label">
Content type
</div>
<ul class="item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
</ul>
</div>
<div class="item-list">
<div class="item-list__label">
Content type
</div>
No content types available.
</div>
Ordered and unordered
`#list_type` defaults to `ul` and `ol` is the only other value the template does anything sensible with -- it is interpolated straight into the tag, so it is a tag name and not a flag. The class goes on whichever element results, so `item-list__list` is on the `ol` too and a stylesheet does not have to name both. Core reaches for `ol` in four places and they are all sequences rather than sets: the numbered steps `Drupal\system\Controller\DbUpdateController::info()` puts in front of the update runner, the migration overview, and search results. Note that the theme puts nothing on `ol` from `item-list.css`: the numbering, the indent and the marker come from the bare element styles of the theme, or from `presetTypography` when the list is inside formatted text, and this component adds none of it.
- Article
- Basic page
- Task
<div class="item-list">
<ul class="item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
</ul>
</div>
- Back up your database. This process will change values in your database.
- Back up your code.
- Put your site into maintenance mode.
<div class="item-list">
<ol class="item-list__list">
<li>Back up your database. This process will change values in your database.</li>
<li>Back up your code.</li>
<li>Put your site into maintenance mode.</li>
</ol>
</div>
The comma list
`#context` is a free-form bag the template mostly ignores; `list_style` is the one key it reads, and it turns into two classes -- `item-list--comma-list` on the wrapper, `item-list__comma-list` on the list. Core sets it in eleven places and never to anything but `comma-list`: the *Requires* and *Required by* columns of the modules page, the *Used in* column of the field storage list, both views UI reports, and the summary line inline form errors puts at the top of a form. The rules used to arrive from `system/base`. They do not any more: `mantra_starter.info.yml` sets `system/base: {css: {component: {css/components/item-list.module.css: false}}}` and re-ships the same four rules from `libraries/global/component/item-list.css`, so they aggregate with the rest of the theme's component CSS instead of arriving separately ahead of it. Drupal 11.3 splits that file out of `system/base` into a library of its own, `system/item-list`, attached from `template_preprocess_item_list()` so it only loads on a page that has an item list on it. When this site reaches 11.3 the override above will match nothing and has to name the new library instead -- and it will keep matching nothing silently, because `libraries-override` does not complain about a file that is no longer there.
- Article
- Basic page
- Task
<div class="item-list--comma-list item-list">
<ul class="item-list__comma-list item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
</ul>
</div>
- .item-list__comma-list, .item-list__comma-list li
- display: inline
- .item-list__comma-list
- margin: 0; padding: 0
- .item-list__comma-list li::after
- content: ", "
- .item-list__comma-list li:last-child::after
- content: ""
One of those four declarations no longer does what it says. `display: inline` is set on the list, but the list is a child of `.item-list`, and `.item-list` is now `display: grid` -- so the list is a grid item, and a grid item's `display` is blockified: `inline` computes to `block`. The commas survive, because they come from `li::after` and the `li` are children of the list rather than of the grid, but the list itself takes a line of its own. `Drupal\inline_form_errors\FormErrorHandler::displayErrorMessages()` is the call site that minds: it renders "3 errors have been found: " and the comma list into a single message string and expects the list to finish the sentence.
- Title
- Body
- Authored by
3 errors have been found:
<div class="item-list--comma-list item-list">
<ul class="item-list__comma-list item-list__list">
<li>Title</li>
<li>Body</li>
<li>Authored by</li>
</ul>
</div>
- Title
- Body
- Authored by
3 errors have been found:
<div class="item-list--comma-list item-list">
<ul class="item-list__comma-list item-list__list">
<li>Title</li>
<li>Body</li>
<li>Authored by</li>
</ul>
</div>
Nesting, and the shorthand for it
`template_preprocess_item_list()` inherits downwards. A child of an item that is not a fully-specified render array -- no `#type`, no `#theme`, no `#markup` -- is taken to be a nested list: its own children that are not properties are moved into `#items` for it, and it is handed the parent's `#theme`, theme hook suggestions and all, and the parent's `#list_type`. So the two demos below are the same markup written two ways, and the shorthand is the one the docblock in `theme.inc` is advertising. The thing to notice in the output is that a nested list is a whole `div.item-list` inside the `li`, not a bare `ul`: that is what puts the four declarations of the second half of this page inside the nesting as well as around it, and it is why a nested list that follows another nested list gets the adjacent-sibling margin.
- Article
- Basic page
- Title
- Body
- Task
<div class="item-list">
<ul class="item-list__list">
<li>Article</li>
<li>
Basic page
<div class="item-list">
<ul class="item-list__list">
<li>Title</li>
<li>Body</li>
</ul>
</div>
</li>
<li>Task</li>
</ul>
</div>
- Article
- Basic page
- Title
- Body
- Task
<div class="item-list">
<ul class="item-list__list">
<li>Article</li>
<li>
Basic page
<div class="item-list">
<ul class="item-list__list">
<li>Title</li>
<li>Body</li>
</ul>
</div>
</li>
<li>Task</li>
</ul>
</div>
Three sets of attributes, three elements
`#wrapper_attributes` on the array goes on the wrapper `div`; `#attributes` goes on the list element; `#wrapper_attributes` on an *item* goes on the `li` of that item. The last is the one core actually uses -- `DbUpdateController::results()` marks every finished update `success` or `failure` that way, `Drupal\user\Form\UserPermissionsForm` puts the module and permission classes on the rows, and `Drupal\menu_ui\MenuForm` marks the enabled checkbox. An item has to be a render array to carry them, which is why core writes an item as a `#markup` and a `#wrapper_attributes` beside it rather than as a plain string. The wrapper set is worth knowing about for one reason beyond classes: `--item-list-line-height` is a custom property on the wrapper, so a `style` attribute there is the supported way to change the leading of a single list without writing a selector for it. That is the last section of this page.
- Converted 12 fields.
- Failed: field_legacy_reference.
<div class="update-results item-list" id="update-9301">
<div class="item-list__label">
Update 9301
</div>
<ul class="update-results__list item-list__list">
<li class="success">Converted 12 fields.</li>
<li class="failure">Failed: field_legacy_reference.</li>
</ul>
</div>
Where core puts them
Four shapes, and they are not interchangeable. Inside a table cell, where the list is the value of a column and the comma style keeps the row one line high. Inside a `details`, as several titled lists in a row, which is the only place in core where two item lists are genuinely adjacent siblings. As a set of theme hook suggestions, which is how search results get their own template and their own classes. And rendered to a string and handed to `Messenger`, which is what `update.manager.inc` and `Drupal\config\Form\ConfigSync` both do -- worth knowing because a message is rendered outside the render context of the page, so anything the list would have bubbled up is discarded on the way.
| Field name | Used in |
|---|---|
| field_body |
|
| field_tags |
|
<div class="table-scroll">
<table class="responsive-enabled" data-striping="1">
<thead>
<tr>
<th>Field name</th>
<th>Used in</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>field_body</td>
<td>
<div class="item-list--comma-list item-list">
<ul class="item-list__comma-list item-list__list">
<li>content</li>
<li>frontpage</li>
</ul>
</div>
</td>
</tr>
<tr class="even">
<td>field_tags</td>
<td>
<div class="item-list--comma-list item-list">
<ul class="item-list__comma-list item-list__list">
<li>taxonomy_term</li>
<li>content_recent</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
Configuration deletions
- node.article.field_tags
- node.page.field_tags
- taxonomy_term
- node.article.default
- node.article.teaser
<details class="js-form-wrapper form-wrapper details" open="open">
<summary role="button" aria-expanded="true">Configuration deletions</summary>
<div class="details-wrapper clearfix">
<div class="details-description">
The listed configuration will be deleted.
</div>
<div class="item-list">
<div class="item-list__label">
Field
</div>
<ul class="item-list__list">
<li>node.article.field_tags</li>
<li>node.page.field_tags</li>
</ul>
</div>
<div class="item-list">
<div class="item-list__label">
View
</div>
<ul class="item-list__list">
<li>taxonomy_term</li>
</ul>
</div>
<div class="item-list">
<div class="item-list__label">
Entity view display
</div>
<ul class="item-list__list">
<li>node.article.default</li>
<li>node.article.teaser</li>
</ul>
</div>
</div>
</details>
- Getting started with the editor
- Uploading an image
<div class="item-list">
<ol class="search-results node_search-results item-list__list">
<li>Getting started with the editor</li>
<li>Uploading an image</li>
</ol>
</div>
<div class="item-list">
<ul class="item-list__list">
<li><a href="/ui-examples/item_lists">Create new account</a></li>
<li><a href="/ui-examples/item_lists">Reset your password</a></li>
</ul>
</div>
The wrapper is a grid
`display: grid` on a wrapper holding a heading and a list is not there for the layout -- one column of two rows is what a block would have done. It is there for the two side effects. A grid container establishes a grid formatting context, so margins inside it never collapse: the heading's bottom margin and the list's top margin both apply and add up, where under a block wrapper the larger of the two would have swallowed the other. That is what let the two rules this file used to carry go -- `> h3 { margin-bottom: .5rem }` and `li + li { margin-top: .25rem }` -- and their removal is the change this page was written for. The second effect is blockification, which is the comma list further up: every child of a grid container has its `display` blockified, so a list that asks to be `inline` is `block` instead. The pair below is the same render array twice, the second with `display: block` put back by this page's own stylesheet.
- Article
- Basic page
- Task
<div class="item-list">
<div class="item-list__label">
Content type
</div>
<ul class="item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
</ul>
</div>
- .item-list
- display: grid
- Article
- Basic page
- Task
<div class="item-list">
<div class="item-list__label">
Content type
</div>
<ul class="item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
</ul>
</div>
A list is not broken across columns
`break-inside: avoid` has no effect on an ordinary page and is the whole point of the wrapper on one that has columns. The page it was added for is the style guide: `libraries/modules/styleguide.css` sets `#styleguide-header.styleguide { columns: 18rem }`, and a column of item lists that is allowed to break puts a heading at the foot of one column and its list at the head of the next, where it reads as the heading of whatever list follows. The declaration is on the wrapper rather than on the list because the heading and the list have to be kept together, and only the wrapper is both of them. Both demos below are the same two lists in the same box, given two columns and a height short enough that something has to break; the second has `break-inside: auto` put back by this page's own stylesheet.
- Article
- Basic page
- Task
- Event
- Basic HTML
- Full HTML
- Restricted HTML
- Plain text
<div class="item-list-demo__columns">
<div class="item-list">
<div class="item-list__label">
Content type
</div>
<ul class="item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
<li>Event</li>
</ul>
</div>
<div class="item-list">
<div class="item-list__label">
Text format
</div>
<ul class="item-list__list">
<li>Basic HTML</li>
<li>Full HTML</li>
<li>Restricted HTML</li>
<li>Plain text</li>
</ul>
</div>
</div>
- .item-list
- break-inside: avoid
- Article
- Basic page
- Task
- Event
- Basic HTML
- Full HTML
- Restricted HTML
- Plain text
<div class="item-list-demo__columns item-list-demo__columns--split">
<div class="item-list">
<div class="item-list__label">
Content type
</div>
<ul class="item-list__list">
<li>Article</li>
<li>Basic page</li>
<li>Task</li>
<li>Event</li>
</ul>
</div>
<div class="item-list">
<div class="item-list__label">
Text format
</div>
<ul class="item-list__list">
<li>Basic HTML</li>
<li>Full HTML</li>
<li>Restricted HTML</li>
<li>Plain text</li>
</ul>
</div>
</div>
The leading, and how to change it
The wrapper sets `--item-list-line-height: 1` and reads it back on the next line. Written that way rather than as a bare `line-height: 1` for one reason: the property is on the wrapper, so it can be overridden per list from a `style` attribute -- which an item list can carry through `#wrapper_attributes` -- or from a selector one level up, without either of them having to restate the `line-height` declaration or beat it on specificity. A line height of `1` is the right default for what item lists mostly hold, which is short labels: machine names, module names, entity ids. It is the wrong one for the case where an item is a sentence, because the two lines of a wrapped item then touch, and there is nothing between one item and the next either. Both demos below hold the same three long items.
- Back up your database. This process will change values in your database, and there is no undo.
- Put your site into maintenance mode, so that nobody reads a half-updated page.
- Install your new files in the appropriate location, as described in the handbook.
<div class="item-list">
<ul class="item-list__list">
<li>Back up your database. This process will change values in your database, and there is no undo.</li>
<li>Put your site into maintenance mode, so that nobody reads a half-updated page.</li>
<li>Install your new files in the appropriate location, as described in the handbook.</li>
</ul>
</div>
- .item-list
- --item-list-line-height: 1; line-height: var(--item-list-line-height)
- Back up your database. This process will change values in your database, and there is no undo.
- Put your site into maintenance mode, so that nobody reads a half-updated page.
- Install your new files in the appropriate location, as described in the handbook.
<div style="--item-list-line-height: 1.5" class="item-list">
<ul class="item-list__list">
<li>Back up your database. This process will change values in your database, and there is no undo.</li>
<li>Put your site into maintenance mode, so that nobody reads a half-updated page.</li>
<li>Install your new files in the appropriate location, as described in the handbook.</li>
</ul>
</div>
Two lists in a row
`.item-list + .item-list` is the only spacing the component has, and it is spacing *between* lists rather than around one: a single item list on a page has no margin of its own from this stylesheet at all, and takes whatever the thing containing it gives it. The selector is the adjacent sibling combinator, so it wants two wrappers next to each other with nothing between them -- which, since the theme's template wraps every list including a nested one, reaches the *Configuration deletions* pane further up this page and a nested list that follows another nested list inside the same `li`. Put anything at all between two lists -- a paragraph, a heading, an empty `div` a layout added -- and the rule stops matching and the gap disappears. That is the second demo, and it is worth having seen once, because the markup that causes it is usually a wrapper somebody added for an unrelated reason.
- node.article.field_tags
- node.page.field_tags
- taxonomy_term
<div class="item-list">
<div class="item-list__label">
Field
</div>
<ul class="item-list__list">
<li>node.article.field_tags</li>
<li>node.page.field_tags</li>
</ul>
</div>
<div class="item-list">
<div class="item-list__label">
View
</div>
<ul class="item-list__list">
<li>taxonomy_term</li>
</ul>
</div>
- .item-list + .item-list
- margin-top: calc(var(--spacing) * 4)
- node.article.field_tags
- node.page.field_tags
The listed configuration will be deleted.
- taxonomy_term
<div class="item-list">
<div class="item-list__label">
Field
</div>
<ul class="item-list__list">
<li>node.article.field_tags</li>
<li>node.page.field_tags</li>
</ul>
</div>
<p>The listed configuration will be deleted.</p>
<div class="item-list">
<div class="item-list__label">
View
</div>
<ul class="item-list__list">
<li>taxonomy_term</li>
</ul>
</div>
Every rule, and where it lives
Six rules, and it takes four files to explain them. `libraries/global/component/item-list.css` is the source and is written in UnoCSS directives, so `@apply grid break-inside-avoid` is what is on disk and the declarations below are what the build turns it into -- read `dist/component.css` to see the real thing, and remember that editing the source changes nothing until `ddev theme build` has run and the site's CSS aggregate has been rebuilt. `libraries/global/component.css` is what imports it. `mantra_starter.info.yml` is where the last four rules come from being the theme's rather than core's, through the `libraries-override` that switches off `system/base`'s copy of `item-list.module.css`. And `templates/dataset/item-list.html.twig` is what puts `.item-list` on an element in the first place, which core's own template does not.
- .item-list
- --item-list-line-height: 1; line-height: var(--item-list-line-height); break-inside: avoid; display: grid
- .item-list + .item-list
- margin-top: calc(var(--spacing) * 4)
- .item-list__comma-list, .item-list__comma-list li
- display: inline
- .item-list__comma-list
- margin: 0; padding: 0
- .item-list__comma-list li::after
- content: ", "
- .item-list__comma-list li:last-child::after
- content: ""