Navigation

v0.0.4
Content

List

Rows of things with media, words and something at the end.

On this page

A list is rows of things, each with something to recognize it by, its words, and something at its end: members, files, integrations, settings. Rows sit on hairlines that start at the text, not under the media, so the column of avatars or icons reads as one edge. A list has no surface of its own; put it in a card or a sheet.

List

<section class="card">  <header class="card-header">    <h3 class="card-title">Members</h3>    <p class="card-description">People with access to this project.</p>  </header>  <ul class="list">    <li class="list-row">      <span class="avatar list-media">        <img class="avatar-image" src="/demo/portrait-1.svg" alt>      </span>      <div class="list-body">        <p class="list-title">Ada Lovelace</p>        <p class="list-description">ada@example.com</p>      </div>      <span class="list-end">        <span class="badge">Owner</span>      </span>    </li>    <li class="list-row">      <span class="avatar list-media">        <img class="avatar-image" src="/demo/portrait-2.svg" alt>      </span>      <div class="list-body">        <p class="list-title">Alan Turing</p>        <p class="list-description">alan@example.com</p>      </div>      <span class="list-end">Admin</span>    </li>    <li class="list-row">      <span class="avatar list-media" aria-hidden="true">GH</span>      <div class="list-body">        <p class="list-title">Grace Hopper</p>        <p class="list-description">grace@example.com</p>      </div>      <span class="list-end">Member</span>    </li>  </ul></section>

#Parts

  • list-media is what the row is recognized by: an avatar, an icon or a thumbnail. It keeps its size.
  • list-body holds a list-title and an optional list-description. Each stays on one line and ends in an ellipsis when the row is narrow.
  • list-end is a value, a status or a control at the end of the row.

The hairline under a row starts where the body starts. Where the browser supports anchor positioning, the body names itself as an anchor scoped to its row and the line is drawn from that edge; elsewhere the line spans the row.

#Settings

With a switch at the end, a list is a settings panel. Label the switch from the row's title.

Settings

<ul class="list">  <li class="list-row">    <div class="list-body">      <p class="list-title" id="set-preview">Preview deployments</p>      <p class="list-description">Every pull request gets its own URL.</p>    </div>    <span class="list-end">      <input type="checkbox" role="switch" class="switch" checked aria-labelledby="set-preview">    </span>  </li>  <li class="list-row">    <div class="list-body">      <p class="list-title" id="set-comments">Comments</p>      <p class="list-description">Let reviewers leave notes on a preview.</p>    </div>    <span class="list-end">      <input type="checkbox" role="switch" class="switch" aria-labelledby="set-comments">    </span>  </li></ul>

#Interactive

list-interactive is for rows that open or select: put list-row on the <a> or <button> itself. Each row becomes a rounded target with room at its sides and answers the pointer at once; the hairlines step aside, since the rows now part themselves. The current row has aria-current.

Interactive

<nav class="list list-interactive max-w(360) mx-auto" aria-label="Projects">  <a class="list-row" href="#atlas" aria-current="page">    <span class="avatar avatar-square list-media" aria-hidden="true">AT</span>    <span class="list-body">      <span class="list-title">Atlas</span>      <span class="list-description">atlas.dev</span>    </span>    <span class="list-end">2h</span>  </a>  <a class="list-row" href="#borealis">    <span class="avatar avatar-square list-media" aria-hidden="true">BO</span>    <span class="list-body">      <span class="list-title">Borealis</span>      <span class="list-description">borealis.dev</span>    </span>    <span class="list-end">1d</span>  </a>  <a class="list-row" href="#cirrus">    <span class="avatar avatar-square list-media" aria-hidden="true">CI</span>    <span class="list-body">      <span class="list-title">Cirrus</span>      <span class="list-description">cirrus.dev</span>    </span>    <span class="list-end">6d</span>  </a></nav>

#Choosing a component

  • A table when rows are compared column by column.
  • A nav for a short list of destinations with icons and no second line.
  • A listbox for options chosen with the keys.

#Accessibility

  • Use a <ul> or an <ol>, so the number of rows is announced.
  • When the name is written in the row, the avatar beside it is decoration: alt="" or aria-hidden="true".
  • A row that is one link must not hold other controls. Give a row with its own controls, such as a switch or a menu button, a plain <li>.

#Classes

ClassDescription
listThe list
list-rowA row: a list item, or a link or a button when the whole row goes somewhere
list-mediaWhat the row is recognized by, at its start: an avatar, an icon or a thumbnail
list-bodyThe words: a title and an optional description
list-titleThe name of the row, on one line that ends in an ellipsis
list-descriptionA quieter second line, on one line that ends in an ellipsis
list-endA value, a status or a control at the end of the row
list-interactiveRows that open or select: each is a rounded target that answers the pointer and a press, at least 44px tall on a touch screen, and the hairlines are not drawn. With list

#Custom properties

PropertyDefaultDescription
--list-padding-y.75remBlock padding of a row
--list-gap.75remSpace between media, words and end

#Source

@master/ui/components/list.css
/* * List: rows of things, each with something to recognize it by, its words, and something at its end: * members, files, settings. Rows sit on hairlines that start at the text, not under the media, so the * column of avatars or icons reads as one edge. A list has no surface of its own. */@layer components {  .list {    --list-padding-y: .75rem;    --list-gap: .75rem;    --icon-size: 1.125rem;    display: flex;    flex-direction: column;    min-width: 0;    margin: 0;    padding: 0;    color: var(--color-text-body);    font-size: .875rem;    line-height: 1.45;    list-style: none;  }  /* A row: a list item, or a link or a button when the whole row goes somewhere. */  .list-row {    --icon-color: var(--color-text-muted);    position: relative;    display: flex;    align-items: center;    gap: var(--list-gap);    min-width: 0;    margin: 0;    padding: var(--list-padding-y) 0;    border: 0;    background: transparent;    color: inherit;    font: inherit;    font-feature-settings: inherit;    text-align: start;    text-decoration: none;    appearance: none;  }  /* What the row is recognized by: an avatar, an icon or a thumbnail. */  .list-media {    flex: none;  }  /* The words. */  .list-body {    display: grid;    flex: 1 1 0;    gap: .0625rem;    min-width: 0;  }  /*   * The hairline under a row. It spans the row, and where anchor positioning exists it starts at the body's   * edge instead: the body names itself as an anchor, scoped to its own row.   */  .list-row:not(:last-child)::after {    content: '';    position: absolute;    inset-inline: 0;    bottom: 0;    height: 1px;    background: var(--color-line-hair);    pointer-events: none;  }  @supports (anchor-scope: --list-body) {    .list-row {      anchor-scope: --list-body;    }    .list-body {      anchor-name: --list-body;    }    .list-row:not(:last-child)::after {      inset-inline-start: anchor(--list-body start);    }  }  .list-title {    margin: 0;    overflow: hidden;    color: var(--color-text-strong);    font-weight: var(--font-weight-control);    text-overflow: ellipsis;    white-space: nowrap;  }  .list-description {    margin: 0;    overflow: hidden;    color: var(--color-text-muted);    font-size: .8125rem;    text-overflow: ellipsis;    white-space: nowrap;  }  /* A value, a status or a control at the end of the row. */  .list-end {    display: inline-flex;    flex: none;    align-items: center;    gap: .5rem;    color: var(--color-text-muted);    font-size: .8125rem;    font-variant-numeric: tabular-nums;  }  /*   * Interactive: rows that open or select. Each is a rounded target with room at its sides, and answers the   * pointer at once; the hairlines step aside, since the rows now part themselves.   */  .list-interactive {    gap: 2px;    .list-row {      padding-inline: .625rem;      border-radius: calc(var(--radius-control) * var(--corner-scale));      corner-shape: var(--corner-shape);      -webkit-tap-highlight-color: transparent;      cursor: pointer;      touch-action: manipulation;      /* A row that is pressed is a touch target. */      @media (pointer: coarse) {        min-height: 2.75rem;      }      @media (hover: hover) {        &:hover {          --icon-color: currentColor;          background: var(--color-surface-chip);        }      }      /* Pressed: a step deeper than the pointer's tone, so a tap is answered where there is no pointer. */      &:active {        background: var(--color-surface-inset-hover);      }      &:is([aria-current="page"], [aria-current="true"], [aria-selected="true"]) {        --icon-color: currentColor;        background: var(--color-surface-chip);      }      &:focus-visible {        outline: 2px solid var(--color-focus);        outline-offset: -2px;      }    }    .list-row::after {      display: none;    }  }  /* A row that cannot be opened now steps back and stops answering the pointer. */  .list-interactive .list-row:is(:disabled, [aria-disabled="true"]) {    background: transparent;    color: var(--color-text-disabled);    cursor: not-allowed;  }  @media (forced-colors: active) {    .list-interactive .list-row:is([aria-current="page"], [aria-current="true"], [aria-selected="true"]) {      background: Highlight;      color: HighlightText;      forced-color-adjust: none;    }  }}