Skip to content

Components

List

Lists are continuous, vertical indexes of text and images and video.

  • Use lists to help users find a specific item and act on it
  • Order list items in logical ways (like alphabetical or numerical)
  • Three sizes: one-line, two-line, and three-line
  • Keep items short and easy to scan
  • Show icons, text, and actions in a consistent format
  • Headline

  • Headline

    Supporting text that truly is quite long enough to fill up multiple lines.

  • Trailing supporting text

    100+
  • Trailing keyboard command

    CTRL+Shift+X
  • Headline with start icon

  • Headline with start icon

    Supporting text that truly is quite long enough to fill up multiple lines.

  • Inset class

    Makes the text line up nicely

  • Link list item

  • OP

    Headline

  • Headline

    Supporting text

  • Link with start icon

  • End icon button

Configurations

A List item is split up in three parts:

  • .text: main content
  • .start (optional): items before the main content
  • .end (optional): items after the main content

Clickable List item

Wrap the elements of your List item with a a, button or label depending on use-case.

html
<li>
  <button><!--  --></button>
</li>

e<li>
  <a><!--  --></a>
</li>

<li>
  <label><!--  --></label>
</li>

Text

Main text lives in div.text.

  • Headline

  • Headline

    Supporting text

  • Headline

    Supporting text that truly is quite long enough to fill up multiple lines.

html
<li>
  <div class="text">
    <p>Headline</p>
    <p>
      Supporting text that truly is quite long enough to fill up multiple lines.
    </p>
  </div>
</li>

Start items

Found in div.start.

Icon

  • Headline

  • Headline

    Supporting text

html
<li>
  <div class="start">
    <svg></svg>
  </div>

  <div class="text">
    <p>Headline</p>
  </div>
</li>

Avatar

Read more: Avatar

  • AB

    Headline

  • Headline

    Supporting text

html
<li>
  <div class="start">
    <div class="avatar"></div>
  </div>

  <div class="text">
    <p>Headline</p>
  </div>
</li>

Image

  • Headline

    Supporting text

  • Headline

    Supporting text

html
<li>
  <div class="start">
    <img />
  </div>

  <div class="text">
    <p>Headline</p>
    <p>Supporting text</p>
  </div>
</li>

Video

  • Headline

    Supporting text

    13:37
  • Headline

    Supporting text

    90s
html
<li>
  <div class="start">
    <video><source /></video>
  </div>

  <div class="text">
    <p>Headline</p>
    <p>Supporting text</p>
  </div>
</li>

End items

Found in div.end.

Text

  • Headline

    30kB
  • Headline

    Supporting text

    99%
  • Headline

    Supporting text that truly is quite long enough to fill up multiple lines.

    100+
html
<li>
  <div class="text">
    <p>Headline</p>
  </div>

  <div class="end">30kB</div>
</li>

Keyboard command

  • Save All

    CTRL+ALT+DEL
  • Save

    CTRL+S
html
<li>
  <div class="text">
    <p>Save all</p>
  </div>

  <div class="end">
    <kbd>CTRL+ALT+DEL</kbd>
  </div>
</li>

Checkbox

Wrap the List item content with a <label class="checkbox" for="INPUTID"> to make the entire surface clickable.

Read more: Checkbox

html
<li>
  <label class="checkbox" for="checkbox-example-1">
    <div class="text"></div>
    <div class="end">
      <input id="checkbox-example-1" type="checkbox" />
    </div>
  </label>
</li>

Radio

Wrap the List item content with a <label class="radio" for="INPUTID"> to make the entire surface clickable.

Radio group: Add a common name to each <input> for radio group behavior.

Read more: Radio

Switch

Read more: Switch

Inset

Enables a list item without a start icon to align with items that do.

  • No inset

  • Inset class

    Makes the text line up nicely

  • Inset class

    Any div.start will be hidden when inset

html
<li class="inset">
  <!--  -->
</li>

Gutterless

Apply the .gutterless class on the ul.list element to remove the inline padding on the list items.

  • Delete

  • Headline

    Supporting text

    100+
html
<ul class="list gutterless">
  <!--  -->
</ul>

Borders

On every item

Apply the .bordered class on the ul.list element to give all list items a border.

  • So

  • Many

  • Borders

html
<ul class="list bordered">
  <!--  -->
</ul>

On one item

Apply the .border-top class on a li.list item to give it an upper border.

  • I need borders

  • Help

  • Thanks

html
<ul class="list">
  <li></li>
  <li></li>
  <li class="border-top"></li>
</ul>

Dense

Simply add the .dense class to the ul element.

  • Headline

  • Headline

    Supporting text that truly is quite long enough to fill up multiple lines.

  • Trailing supporting text

    100+
  • Trailing keyboard command

    CTRL+Shift+X
  • Headline with start icon

  • Headline with start icon

    Supporting text that truly is quite long enough to fill up multiple lines.

  • Inset class

    Makes the text line up nicely

  • Link list item

  • OP

    Headline

  • Headline

    Supporting text

  • Link with start icon

  • End icon button

html
<ul class="list dense">
  <!--  -->
</ul>

Anatomy

  1. Container: ul.list
  2. List item: li
  3. Content wrapper (optional): a, button, label
  4. Start content (optional): .start > svg, img, video
  5. Text content: .text > p, p + p
  6. End content (optional): .end > svg, p, button, a, input
  • Text

    15%
html
<ul class="list">
  <li>
    <!-- 3 -->

    <div class="start">
      <!-- 4 -->
    </div>

    <div class="text">
      <!-- 5 -->
    </div>

    <div class="end">
      <!-- 6 -->
    </div>
  </li>
</ul>

API

TypeModifiersDefaultDescription
Denseul.dense-When enabled list appears tighter packed
Gutterlessul.gutterless-When enabled list inline padding is removed
Borderedul.bordered-When enabled a border is rendered on all list items

List item

TypeModifiersDefaultDescription
Main partsli > .start, li > .text, li > .end-Building blocks in List item
Insetli.inset-When enabled a list item without a start icon algins with items that do
Border topli.border-top-When enabled the list item will get a top border

Browser compatibility

Installation

css
@layer components.has-deps {
  /*
Lists meant to be used stand-alone or as part of Select elements

  Intended use-case:
  - ul.list > li
  - .select > .list > option
*/
  :where(.list) {
    --_bg-color: light-dark(var(--gray-1), var(--gray-15));

    background-color: var(--_bg-color);
    list-style: none;
    padding: var(--size-2) 0;

    @media (pointer: coarse) {
      &,
      * {
        user-select: none;
      }
    }

    /* Borders on all list items */
    &.bordered {
      :where(li + li, option + option) {
        margin-block-start: var(--size-3);
        &:before {
          block-size: 1px;
          border-block-start: 1px solid var(--border-color);
          content: "";
          display: block;
          inline-size: 100%;
          inset: calc(-1 * var(--size-2)) 0 auto 0;
          position: absolute;
          visibility: visible; /* override select > option:before style */
        }
      }
    }

    /* Dense - less gaps and spacing */
    &.dense {
      :where(li, option) {
        gap: var(--size-2);
        min-block-size: var(--size-7);
        padding: var(--size-1) var(--size-2);

        &.border-top {
          margin-block-start: var(--size-2);
          &:before {
            inset: calc(-1 * var(--size-1)) 0 auto 0;
          }
        }

        /* Clickable list item */
        &:has(> a, > button, > label) {
          min-block-size: auto;
          padding: 0;
        }

        & > :where(a, button, label) {
          gap: var(--size-2);
          min-block-size: var(--size-7);
          padding: var(--size-1) var(--size-2);
        }

        /* Checkbox / Radio */
        & > label {
          .end {
            padding-inline-end: 0.125rem;
          }
        }

        /* Leading and trailing content */
        .start,
        .end {
          .avatar {
            max-inline-size: var(--size-6);
          }

          .icon-button,
          svg {
            max-inline-size: var(--size-4);
          }

          .checkbox,
          .radio {
            max-inline-size: var(--size-3);
          }
        }
      }
    }

    /* Gutterless */
    &.gutterless {
      :where(li, option) {
        padding-inline: 0;

        & > :where(a, button, label) {
          padding-inline: 0;
        }
      }
    }

    /* List item */
    :where(li, option, [role="group"] > label) {
      align-items: center;
      background: var(--_bg-color) var(--ripple, none);
      display: flex;
      font-size: var(--font-size-sm);
      gap: var(--size-3);
      isolation: isolate;
      min-block-size: 40px;
      padding: var(--size-2) var(--size-3);
      position: relative;

      &:before {
        display: none; /* removing checkmark from option */
      }

      * {
        font-size: inherit;
      }

      /* Clickable list item */
      &:has(> a, > button, > label) {
        background: transparent;
        display: block;
        min-block-size: auto;
        padding: 0;
      }

      /* Select option */
      &:where(option) {
        align-items: center;
        background-color: var(--_bg-color);
        color: inherit;
        cursor: pointer;
        display: flex;
        gap: var(--size-3);
        inline-size: 100%;
        margin: 0;
        min-block-size: 40px;
        padding: var(--size-2) var(--size-3);
        text-align: start;
        text-decoration: none;
        z-index: 0;

        &:hover {
          background-color: light-dark(var(--gray-2), var(--gray-14));
        }

        &:checked {
          background-color: oklch(from var(--primary) l c h / 30%);
        }
      }

      & > a,
      & > button,
      & > label {
        align-items: center;
        background: var(--_bg-color) var(--ripple, none);
        color: inherit;
        cursor: pointer;
        display: flex;
        gap: var(--size-3);
        inline-size: 100%;
        margin: 0;
        min-block-size: 40px;
        padding: var(--size-2) var(--size-3);
        text-align: start;
        text-decoration: none;
        z-index: 0;

        /*** Ripple effect */
        background-position: center;
        transition: background var(--button-ripple-duration);
        &:where(:not(:active):hover) {
          --ripple: radial-gradient(circle, transparent 1%, var(--_bg-color) 1%)
            center/15000%;
        }

        &:where(:hover:active) {
          background-size: var(--button-ripple-size);
          transition: background 0s;
        }

        &:hover {
          background-color: light-dark(var(--gray-2), var(--gray-14));
        }

        /*** Remove ripple effect when trailing button is clicked */
        &:has(.end:hover) {
          &:where(:not(:active):hover) {
            --ripple: none;
          }
        }
      }

      /* Checkbox / Radio / Switch */
      & > label {
        .end {
          padding-inline-end: var(--size-1);
        }

        &:where(.checkbox, .radio) {
          inline-size: 100%;
        }

        &.switch {
          --_dot-size: 0.75rem;
          --_track-height: var(--size-4);
          --_track-width: 2.5rem;
        }
      }

      /* Video */
      &:has(video) {
        padding: 0.75rem var(--size-3) 0.75rem 0;
      }

      /* Border between list items */
      &.border-top {
        margin-block-start: var(--size-3);
        &:before {
          block-size: 1px;
          border-block-start: 1px solid var(--border-color);
          content: "";
          display: block;
          inline-size: 100%;
          inset: calc(-1 * var(--size-2)) 0 auto 0;
          position: absolute;
        }
      }

      /* Text */
      .text {
        flex: 1;
        line-height: 1.6;

        :where(h1, h2, h3, h4, h5, h6, p, span) {
          color: inherit;
          font-weight: 400;
        }

        p + p {
          font-size: var(--font-size-xs);
        }
      }

      /* Leading content */
      .start {
        align-self: center;
        align-items: center;
        display: grid;
        z-index: 1;

        &:has(svg) {
          max-inline-size: var(--size-5);
        }

        svg {
          padding-block-start: 0.125rem;
        }

        img {
          aspect-ratio: 1;
          inline-size: 56px;
          object-fit: cover;
        }

        video {
          aspect-ratio: 16/9;
          block-size: 64px;
          object-fit: cover;
        }
      }

      /* Trailing content */
      .end {
        align-items: center;
        display: flex;
        font-size: var(--font-size-xs);
        text-align: end;
        z-index: 1;

        &:not(:has(a, button, input)) {
          pointer-events: none;
        }

        kbd {
          background-color: transparent;
          border: 0;
          color: inherit;
          opacity: 0.6;
        }

        svg {
          max-inline-size: var(--size-5);
          inline-size: 100%;
        }
      }

      /* Inset */
      &.inset {
        .text {
          padding-inline-start: calc(var(--size-5) + var(--size-3));
        }

        /* Safety measure so it won't look bad if there for some reason should exist a leading element inside. */
        .start {
          display: none;
        }
      }
    }
  }
}