Navigation

v0.0.4
Data display

Gantt

Things that last, as bars on one scale of time.

On this page

A gantt shows things that last as bars on one scale of time: the phases of a project, on-call shifts, a release train. It is a list. Each row carries where its bar begins and ends, its name, and what the bar says as text, and the stylesheet does the placing: names in one column, bars in the other, a hairline at every unit of the scale. There is no script and no SVG.

Gantt

<div class="card max-w(560) mx-auto">  <ol class="gantt gantt-now" style="--gantt-from: 0; --gantt-to: 8; --gantt-now: 4.4" aria-label="Launch plan, in weeks">    <li class="gantt-row gantt-scale" aria-hidden="true">      <ol class="gantt-ticks">        <li>W1</li>        <li>W2</li>        <li>W3</li>        <li>W4</li>        <li>W5</li>        <li>W6</li>        <li>W7</li>        <li>W8</li>      </ol>    </li>    <li class="gantt-row" style="--from: 0; --to: 3; --progress: 100">      <span class="gantt-label">Research</span>      <span class="gantt-bar">Weeks 1 to 3</span>    </li>    <li class="gantt-row" aria-current="true" style="--from: 2; --to: 6">      <span class="gantt-label">Design</span>      <span class="gantt-bar">Weeks 3 to 6</span>    </li>    <li class="gantt-row" style="--from: 4; --to: 7.5; --progress: 15">      <span class="gantt-label">Build</span>      <span class="gantt-bar">Weeks 5 to 8</span>    </li>    <li class="gantt-row" style="--from: 7.5">      <span class="gantt-label">Launch</span>      <span class="gantt-bar gantt-milestone">Middle of week 8</span>    </li>  </ol></div>

#How a bar gets its place

The scale runs from --gantt-from to --gantt-to, set on the list. The unit is yours: days, weeks, hours, sprints. Each gantt-row sets --from and --to in the same unit, and its gantt-bar begins and ends at that share of the track. Fractions work, so a bar can end in the middle of a unit.

The names take the width of the longest one and the track takes the rest; every row shares the two columns, so the bars line up without a width written anywhere. A hairline marks each unit of the scale behind the bars.

A bar's text is what the bar says: its dates, its length, who has it. Write it even when the bar is too short to show it, since it is what a screen reader reads; text that does not fit ends in an ellipsis.

#Progress, the current row and now

--progress on a row, from 0 to 100, fills the bar with a firmer tint that far. The row marked aria-current="true" has an ink bar: the one phase a reader is in, or the one a view is about.

gantt-now on the list draws an ink line down every row at --gantt-now, for today. Under the pointer a row's bar is a step firmer and its name takes the strong ink, so a reader can follow one row across a wide chart.

#Milestones

A milestone is a point, not a span. Put gantt-milestone on the bar of a row that sets only --from: it is drawn as a small ink diamond on that point. Its text stays in the list for assistive technology and is not shown, so name the row for what happens there.

#The scale

A row with gantt-scale names the units. It holds one gantt-ticks list with an item for each unit, and each name stands over the hairline that begins its unit. It is a picture of what the bars already say in words, so hide it with aria-hidden.

Hours of a day

<div class="card max-w(560) mx-auto">  <ol class="gantt" style="--gantt-from: 0; --gantt-to: 24" aria-label="On call today">    <li class="gantt-row gantt-scale" aria-hidden="true">      <ol class="gantt-ticks">        <li>00</li>        <li></li>        <li></li>        <li></li>        <li></li>        <li></li>        <li>06</li>        <li></li>        <li></li>        <li></li>        <li></li>        <li></li>        <li>12</li>        <li></li>        <li></li>        <li></li>        <li></li>        <li></li>        <li>18</li>        <li></li>        <li></li>        <li></li>        <li></li>        <li></li>      </ol>    </li>    <li class="gantt-row" style="--from: 0; --to: 8">      <span class="gantt-label">Grace</span>      <span class="gantt-bar">00:00 to 08:00</span>    </li>    <li class="gantt-row" aria-current="true" style="--from: 8; --to: 16">      <span class="gantt-label">Ada</span>      <span class="gantt-bar">08:00 to 16:00</span>    </li>    <li class="gantt-row" style="--from: 16; --to: 24">      <span class="gantt-label">Alan</span>      <span class="gantt-bar">16:00 to 24:00</span>    </li>  </ol></div>

#In a narrow container

The gantt is a size container. Under 26rem of its own width each name stands over its bar instead of beside it, so the track keeps the whole width. Keep the scale short on a phone: eight units read, thirty do not.

#Gantt, timeline or chart

A gantt is for things with a beginning and an end. Moments in the order they happened are a timeline. Amounts compared with each other, with no place in time, are a chart.

#Accessibility

  • Write it as an <ol> and name it with aria-label or a heading. Each row is an item whose name and bar text are read in order: "Design, weeks 3 to 6".
  • The bars, the hairlines and the line of now are drawn by the stylesheet. Say in the text what they show: a bar's dates, and that a row is the current one where it matters.
  • Colour is not used: the current row is told apart by ink against a tint, and in forced colors by a filled bar against outlined ones.

#Classes

ClassDescription
ganttThe list: names in one column and bars in the other, on a scale from --gantt-from to --gantt-to with a hairline at every unit
gantt-nowDraws an ink line down the rows at --gantt-now, for today. With gantt
gantt-rowOne row; set --from and --to in its style, and --progress for how far along it is. Under the pointer its bar is a step firmer. In a container under 26rem the name stands over the bar
gantt-scaleThe row that names the scale's units: it holds gantt-ticks and has no track. Hide it from assistive technology when the bars already say their dates. With gantt-row
gantt-labelWhat the row is, at the start; one line that ends in an ellipsis
gantt-barThe bar, 28px tall: a tint of ink from --from to --to, filled with a firmer tint as far as --progress percent. Its text is what the bar says, such as its dates
gantt-milestoneA point in time instead of a span: a small ink diamond at --from. Its text stays for assistive technology. With gantt-bar
gantt-ticksThe names of the scale's units inside gantt-scale, one item to a unit

#Custom properties

PropertyDefaultDescription
--gantt-from0Where the scale begins
--gantt-to12Where the scale ends
--gantt-now0Where the line of gantt-now stands on the scale
--fromWhere the row's bar begins, in the scale's unit. On gantt-row
--toWhere the row's bar ends. On gantt-row
--progressHow much of the bar is done, from 0 to 100. On gantt-row
--gantt-gap.375remSpace between rows
--gantt-colorvar(--color-surface-inverse)The ink the bars are tinted with

#Source

@master/ui/components/gantt.css
/* * Gantt: things that last, as bars on one scale of time. * It is a list. The scale runs from --gantt-from to --gantt-to, in any unit: days, weeks, hours. Each row * carries where its bar begins and ends as --from and --to, its name in gantt-label and what the bar says as * the text of gantt-bar, so the list reads as data to anyone who is not looking at it. The stylesheet does the * placing: names in one column, bars in the other, a hairline at every unit of the scale. * A bar is a tint of ink, 28px tall, and fills with a firmer tint as far as --progress percent. The row * marked aria-current has an ink bar. A milestone is a point, not a span: a small ink diamond at --from. * gantt-now draws an ink line down the rows at --gantt-now. * In a narrow container the name stands over its bar instead of beside it. */@layer components {  .gantt {    --gantt-from: 0;    --gantt-to: 12;    --gantt-now: 0;    --gantt-gap: .375rem;    --gantt-color: var(--color-surface-inverse);    --gantt-span: calc(var(--gantt-to) - var(--gantt-from));    container-type: inline-size;    display: grid;    grid-template-columns: auto minmax(0, 1fr);    gap: var(--gantt-gap) 1rem;    min-width: 0;    margin: 0;    padding: 0;    color: var(--color-text-body);    font: 400 .8125rem/1.25rem var(--font-family-sans);    font-feature-settings: inherit;    font-variant-numeric: tabular-nums;    list-style: none;    /* The line of now: drawn in every row, so it runs the height of the chart. */    &.gantt-now .gantt-row:not(.gantt-scale)::after {      content: '';      z-index: 2;      grid-area: 1 / 2;      align-self: stretch;      justify-self: start;      width: 1px;      margin-block: calc(var(--gantt-gap) / -2);      margin-inline-start: calc((var(--gantt-now) - var(--gantt-from)) / var(--gantt-span) * 100% - .5px);      background: var(--color-text-strong);      pointer-events: none;    }    /* Under the pointer a row's bar is a step firmer and its name takes the strong ink, at once. */    @media (hover: hover) {      .gantt-row:not([aria-current="true"]):hover {        .gantt-bar {          --gantt-tint: color-mix(in oklab, var(--gantt-color) 16%, transparent);          --gantt-done: color-mix(in oklab, var(--gantt-color) 30%, transparent);        }        .gantt-label {          color: var(--color-text-strong);        }      }    }    @container (width < 26rem) {      .gantt-row {        grid-template-columns: minmax(0, 1fr);        row-gap: .125rem;      }      .gantt-label {        grid-area: 1 / 1;        max-width: none;      }      .gantt-row::before, .gantt-bar, .gantt-ticks {        grid-area: 2 / 1;      }      &.gantt-now .gantt-row:not(.gantt-scale)::after {        grid-area: 2 / 1;      }      .gantt-scale .gantt-ticks {        grid-area: 1 / 1;      }    }    @media (forced-colors: active) {      --gantt-color: CanvasText;    }  }  /* One row: the name in the first column, and the track with its bar in the second. */  .gantt-row {    display: grid;    grid-column: 1 / -1;    grid-template-columns: subgrid;    align-items: center;    min-width: 0;    min-height: 1.75rem;    margin: 0;    padding: 0;    /* The scale behind the bar: a hairline at each unit, joined to the row above and below. */    &::before {      content: '';      grid-area: 1 / 2;      align-self: stretch;      margin-block: calc(var(--gantt-gap) / -2);      background: repeating-linear-gradient(90deg, var(--color-line-hair) 0 1px, transparent 1px calc(100% / var(--gantt-span)));      box-shadow: 1px 0 0 var(--color-line-hair);      pointer-events: none;    }    /* Where the scale runs from the right, so do its hairlines, and the closing one is at the left. */    &:dir(rtl)::before {      background: repeating-linear-gradient(270deg, var(--color-line-hair) 0 1px, transparent 1px calc(100% / var(--gantt-span)));      box-shadow: -1px 0 0 var(--color-line-hair);    }    /* The row that matters now: its bar is ink. */    &[aria-current="true"] {      .gantt-bar {        background: var(--gantt-color);        color: var(--color-text-inverse);        box-shadow: var(--shadow-ink-top);      }      .gantt-label {        color: var(--color-text-strong);      }    }    /* The row of the scale itself: its names, and no track of its own. */    &.gantt-scale {      min-height: 1.25rem;      &::before {        content: none;      }    }  }  /* What the row is. */  .gantt-label {    grid-area: 1 / 1;    max-width: 10rem;    overflow: hidden;    text-overflow: ellipsis;    white-space: nowrap;  }  /* The bar: from --from to --to of the scale, filled as far as --progress percent. Its text is what it says. */  .gantt-bar {    --gantt-tint: color-mix(in oklab, var(--gantt-color) 10%, transparent);    --gantt-done: color-mix(in oklab, var(--gantt-color) 22%, transparent);    z-index: 1;    grid-area: 1 / 2;    box-sizing: border-box;    width: calc((var(--to, 1) - var(--from, 0)) / var(--gantt-span) * 100%);    min-width: .25rem;    height: 1.75rem;    margin-inline-start: calc((var(--from, 0) - var(--gantt-from)) / var(--gantt-span) * 100%);    padding: .25rem .5rem;    border-radius: calc(var(--radius-chip) * var(--corner-scale));    corner-shape: var(--corner-shape);    overflow: hidden;    background: linear-gradient(90deg, var(--gantt-done) calc(var(--progress, 0) * 1%), var(--gantt-tint) 0);    color: var(--color-text-strong);    font-weight: var(--font-weight-control);    font-size: .78125rem;    text-overflow: ellipsis;    white-space: nowrap;    /* A bar answers the pointer and is not a line to select. */    cursor: default;    &:dir(rtl) {      background: linear-gradient(270deg, var(--gantt-done) calc(var(--progress, 0) * 1%), var(--gantt-tint) 0);    }    /* A milestone: a point in time, drawn as a diamond on it. Its text is kept for assistive technology. */    &.gantt-milestone {      width: .75rem;      min-width: 0;      height: .75rem;      margin-inline-start: calc((var(--from, 0) - var(--gantt-from)) / var(--gantt-span) * 100% - .375rem);      padding: 0;      border-radius: 2px;      corner-shape: round;      background: var(--gantt-color);      color: transparent;      font-size: 0;      rotate: 45deg;    }    @media (forced-colors: active) {      outline: 1px solid CanvasText;      outline-offset: -1px;      forced-color-adjust: none;      background: Canvas;      color: CanvasText;      &.gantt-milestone {        background: CanvasText;      }    }  }  /* The names of the scale's units, one to a unit, each over the hairline that begins it. */  .gantt-ticks {    display: grid;    grid-area: 1 / 2;    grid-auto-columns: minmax(0, 1fr);    grid-auto-flow: column;    min-width: 0;    margin: 0;    padding: 0;    color: var(--color-text-muted);    font-size: .71875rem;    line-height: 1.25rem;    list-style: none;    white-space: nowrap;  }}