Navigation

v0.0.4
Forms

Checklist

A short list of things to do that counts how many are done.

On this page

A checklist is a short list of things to do: the steps of a setup, the points of a review. Each step is a native checkbox in a label with a title and a line of help. A step that is done steps back, with a hairline through its title, and the list counts itself.

Checklist

<div class="card max-w(400) mx-auto">  <div class="card-header">    <h3 class="card-title">Get Atlas ready</h3>  </div>  <ul class="checklist">    <li class="checklist-item">      <label class="checklist-label">        <input type="checkbox" class="checkbox" checked>        <span class="checklist-body">          <span class="checklist-title">Create a project</span>          <span class="checklist-description">Atlas was created on 3 November.</span>        </span>      </label>    </li>    <li class="checklist-item">      <label class="checklist-label">        <input type="checkbox" class="checkbox" checked>        <span class="checklist-body">          <span class="checklist-title">Connect a repository</span>          <span class="checklist-description">Deploy every push to the main branch.</span>        </span>      </label>    </li>    <li class="checklist-item">      <label class="checklist-label">        <input type="checkbox" class="checkbox">        <span class="checklist-body">          <span class="checklist-title">Add a domain</span>          <span class="checklist-description">Point a domain you own at the project.</span>        </span>        <span class="checklist-end">          <span class="badge badge-sm">2 min</span>        </span>      </label>    </li>    <li class="checklist-item">      <label class="checklist-label">        <input type="checkbox" class="checkbox">        <span class="checklist-body">          <span class="checklist-title">Invite your team</span>          <span class="checklist-description">Members can deploy and read the logs.</span>        </span>      </label>    </li>    <li class="checklist-count">steps done</li>  </ul></div>
Check a step: the count under the list follows, with no script.

#The list counts itself

Two CSS counters do the arithmetic. Every checklist-item adds one to the total, and an item whose checkbox is checked, which the item knows through :has(), adds one to the number done. checklist-count prints both.

Counters run in document order, so the count comes after the steps in the markup: that is where both numbers are final. Its own text follows the numbers, which keeps the words yours to translate. --checklist-of is what stands between the two numbers, ' / ' by default; set it to ' of ' for "2 of 4".

The count is generated content. It is right at every moment, but a change to it is not announced: when finishing the list matters, say so in a status message of your own.

#A step

The label is the whole row, so a press anywhere on the words checks the box. checklist-title is an inline element on purpose: the stroke through a finished title is a background that grows along the text, and it stays one line however the title wraps. checklist-end holds something at the end of a step, such as how long it takes.

Rows part on hairlines that start at the text, as a list's do. The checklist has no surface: put it in a card.

#Accessibility

  • Use a <ul>, or an <ol> when the steps must be done in order.
  • A checkbox here is state the reader sets. For steps the system completes, show them with steps instead: a checkbox that a reader cannot change is a disabled control, not a record.
  • The struck title keeps the muted ink at a readable contrast; the meaning is carried by the checkbox, not by the stroke.

#Classes

ClassDescription
checklistThe list, on a ul, or an ol when the steps are done in order; it counts its steps and the checked ones
checklist-itemA step, on the li; it counts itself, and counts itself done while its checkbox is checked
checklist-labelThe label: the checkbox, then the words. The whole row checks the box, and is 44px tall on a touch screen
checklist-bodyThe words of a step: a title over a description
checklist-titleThe step's name, on an inline element; a hairline is drawn through it when the step is done
checklist-descriptionA muted line of help under the title
checklist-endSomething at the end of a step: a link, a badge, a time
checklist-countSteps done and steps in all, written before its own text; it comes after the last step in the markup

#Custom properties

PropertyDefaultDescription
--checklist-of' / 'What stands between the two numbers of the count
--checklist-gap.75remSpace between the checkbox and the words

#Source

@master/ui/components/checklist.css
/* * Checklist: a short list of things to do, each a native checkbox with a title and a line of help. * A row is a label, so the whole of it checks the box. A step that is done steps back: its title turns to * the muted ink and a hairline is drawn through it from its start, the way a pen would. * The list counts itself. CSS counters add up the rows and, with :has(), the checked ones; checklist-count * writes the two numbers after the rows, so "2 / 5" is always true without a script. * Rows part on hairlines that start at the text, as a list's do. A checklist has no surface of its own. */@layer components {  .checklist {    --checklist-gap: .75rem;    --checklist-padding-y: .6875rem;    /* What stands between the two numbers of the count. */    --checklist-of: ' / ';    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;    counter-reset: checklist-total checklist-done;    /* On touch a row is 44px: the same line of words with a little more room over and under it. */    @media (pointer: coarse) {      --checklist-padding-y: .75rem;    }  }  /* A step. It counts itself, and counts itself done while its checkbox is checked. */  .checklist-item {    position: relative;    min-width: 0;    margin: 0;    padding: 0;    counter-increment: checklist-total;    &:has(.checkbox:checked) {      counter-increment: checklist-total checklist-done;    }    /* The hairline under a step that another follows; it starts where the words do. */    &:has(+ .checklist-item)::after {      content: '';      position: absolute;      inset-inline: calc(1rem + var(--checklist-gap)) 0;      bottom: 0;      height: 1px;      background: var(--color-line-hair);      pointer-events: none;    }  }  /* The label: the checkbox, then the words. The box is level with the first line of the title. */  .checklist-label {    display: flex;    align-items: flex-start;    gap: var(--checklist-gap);    min-width: 0;    padding: var(--checklist-padding-y) 0;    cursor: pointer;    touch-action: manipulation;    -webkit-tap-highlight-color: transparent;    &:has(.checkbox:disabled) {      cursor: not-allowed;    }    .checkbox {      flex: none;      margin-top: calc((1lh - 1rem) / 2);    }  }  .checklist-body {    display: grid;    flex: 1 1 0;    gap: .125rem;    min-width: 0;  }  /*   * The title is inline, so the stroke through it is one line however the title wraps: it is a background   * that grows from nothing to the title's whole length.   */  .checklist-title {    justify-self: start;    background: linear-gradient(currentColor, currentColor) 0 calc(50% + .0625em) / 0 1px no-repeat;    color: var(--color-text-strong);    font-weight: var(--font-weight-control);    transition-property: background-size, color;    transition-duration: var(--duration-base), var(--duration-quick);    transition-timing-function: var(--easing-glide);    @media (prefers-reduced-motion: reduce) {      transition: none;    }  }  .checklist-item:has(.checkbox:checked) .checklist-title {    background-size: 100% 1px;    color: var(--color-text-muted);  }  .checklist-item:has(.checkbox:disabled) .checklist-title {    color: var(--color-text-disabled);  }  .checklist-description {    margin: 0;    color: var(--color-text-muted);    font-size: .8125rem;    text-wrap: pretty;    overflow-wrap: anywhere;  }  /* Something at the end of a step: a link to where it is done, a badge, a time. */  .checklist-end {    display: inline-flex;    flex: none;    align-items: center;    align-self: center;    gap: .5rem;    color: var(--color-text-muted);    font-size: .8125rem;  }  /*   * The count: how many steps are done, of how many. It comes after the steps in the markup, where the   * counters have their final values; its own words follow the numbers.   */  .checklist-count {    margin: .625rem 0 0;    color: var(--color-text-muted);    font-size: .8125rem;    font-variant-numeric: tabular-nums;    &::before {      content: counter(checklist-done) var(--checklist-of) counter(checklist-total) ' ';      color: var(--color-text-strong);      font-weight: var(--font-weight-control);    }  }}