Data

MilestoneList

Milestones down a line with dates, done, current, coming or late, a progress bar on those under way, and the whole project's progress.

#list#progress#schedule#no-js

Demo

Preview

Launch

2 of 5 done
  1. Design signed off (done)

  2. Private alpha (done)

  3. Beta (current)

    Due

    3 of 5 features in

  4. Security review (late)

    Was due

    Waiting on the auditors

  5. Public launch (to come)

    Due
  1. Hire two engineers (to come)

    Due
  2. Open the Porto office (current)

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/milestone_list.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/milestone_list.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/milestone_list.qwe
view MilestoneListDemo() =    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; align-items: start">        <MilestoneList id="milestone-list-demo-launch" title="Launch" items={[            { title: "Design signed off", date: "2026-09-01", status: "done" },            { title: "Private alpha", date: "2026-09-20", status: "done", href: "#milestone_list" },            { title: "Beta", date: "2026-10-15", status: "current", progress: 60, text: "3 of 5 features in" },            { title: "Security review", date: "2026-10-20", status: "late", progress: 25, text: "Waiting on the auditors" },            { title: "Public launch", date: "2026-11-30" },        ]}/>        <MilestoneList id="milestone-list-demo-plain" summary={false} label="Q4" items={[            { title: "Hire two engineers", date: "December" }, { title: "Open the Porto office", status: "current" },        ]}/>    </div>

Source

ui/milestone_list.qwe
// MilestoneList: the milestones of a project down a line: each with its date,// whether it is done, current, coming or late, and how far along the current ones// are; with the whole project's progress at the top.////   <MilestoneList title="Launch" items={[//       { title: "Design signed off", date: "2026-09-01", status: "done" },//       { title: "Beta", date: "2026-10-15", status: "current", progress: 60, text: "3 of 5 features in" },//       { title: "Public launch", date: "2026-11-30" },//   ]}/>//// items: maps of { title, date?, status?, progress?, text?, href? }: status is// "done", "current", "coming" (the default) or "late"; date an ISO date, shown as// "Oct 15, 2026" (other text as it is); progress 0 to 100, a bar under a current// or late one; text a line under the title; href makes the title a link.// title: a heading over the list, with "2 of 5 done" and a bar of the whole.// summary: false leaves that out. label: what screen readers call the list// (the title, else "Milestones"). id: its id. It is an <ol>.import ui/theme.qwe use ui_clslet milestone_list_css = css`.ui-milestone-list { display: grid; gap: 14px; min-width: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-milestone-list-head { display: grid; gap: 6px; }.ui-milestone-list-top { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 12px; }.ui-milestone-list-title { margin: 0; font-size: 16px; font-weight: 650; }.ui-milestone-list-sum { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-milestone-list-bar { height: 6px; border-radius: 999px; background: var(--ui-subtle); overflow: hidden; }.ui-milestone-list-fill { display: block; height: 100%; border-radius: inherit; background: var(--ui-success); }.ui-milestone-list-items { margin: 0; padding: 0; list-style: none; }.ui-milestone-list-item { --t: var(--ui-strong); position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 12px; padding-bottom: 18px; }.ui-milestone-list-item:last-child { padding-bottom: 0; }.ui-milestone-list-item::before { content: ""; position: absolute; left: 11px; top: 24px; bottom: 0; width: 2px; background: var(--ui-border); }.ui-milestone-list-item:last-child::before { display: none; }.ui-milestone-list-done::before { background: var(--ui-success); }.ui-milestone-list-done { --t: var(--ui-success); }.ui-milestone-list-current { --t: var(--ui-accent); }.ui-milestone-list-late { --t: var(--ui-danger); }.ui-milestone-list-mark { display: grid; place-items: center; width: 24px; height: 24px; border: 2px solid var(--t); border-radius: 50%; background: var(--ui-surface); color: #fff; }.ui-milestone-list-mark svg { width: 12px; height: 12px; }.ui-milestone-list-done .ui-milestone-list-mark { background: var(--ui-success); }.ui-milestone-list-current .ui-milestone-list-mark { box-shadow: var(--ui-focus); }.ui-milestone-list-current .ui-milestone-list-mark::after { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--ui-accent); }.ui-milestone-list-late .ui-milestone-list-mark { color: var(--ui-danger); font-size: 13px; font-weight: 800; line-height: 1; }.ui-milestone-list-body { display: grid; gap: 3px; min-width: 0; padding-top: 1px; }.ui-milestone-list-row { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 2px 12px; }.ui-milestone-list-name { margin: 0; font-size: 14px; font-weight: 600; }.ui-milestone-list-name a { color: inherit; text-decoration: none; }.ui-milestone-list-name a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-milestone-list-name a:focus-visible { outline: none; border-radius: 4px; box-shadow: var(--ui-focus); }.ui-milestone-list-coming .ui-milestone-list-name { color: var(--ui-muted); font-weight: 500; }.ui-milestone-list-when { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-milestone-list-late .ui-milestone-list-when { color: var(--ui-danger); font-weight: 600; }.ui-milestone-list-text { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-milestone-list-progress { display: flex; align-items: center; gap: 8px; margin-top: 4px; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-milestone-list-progress .ui-milestone-list-bar { flex: 1; max-width: 240px; }.ui-milestone-list-progress .ui-milestone-list-fill { background: var(--t); }`let milestone_list_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date as "Oct 15, 2026"; other text as it is.fn milestone_list_date(d) -> str {    let p = split(slice(str(d), 0, 10), "-")    len(p) == 3 && int(p[1]) != null && int(p[2]) != null ? `${milestone_list_months[int(p[1]) - 1]} ${int(p[2])}, ${p[0]}` : str(d)}view MilestoneList(items: list = [], title: str? = null, summary: bool = true, label: str? = null, id: str? = null, class: str? = null) {    style global milestone_list_css    let done = count(items, (m) => m.status == "done")    let pct = len(items) == 0 ? 0 : round(done * 100 / len(items))    <div class={ui_cls(["ui-milestone-list", class])} id={id}>        {(title != null || summary) && len(items) > 0 && <div.ui-milestone-list-head>            <div.ui-milestone-list-top>                {title != null && <h3.ui-milestone-list-title>{title}</h3>}                {summary && <span.ui-milestone-list-sum>{done} of {len(items)} done</span>}            </div>            {summary && <div.ui-milestone-list-bar role="progressbar" aria-label={`${title ?? "Milestones"}: done`} aria-valuemin="0"                             aria-valuemax="100" aria-valuenow={pct}><span.ui-milestone-list-fill style={`width: ${pct}%`}></span></div>}        </div>}        <ol.ui-milestone-list-items aria-label={label ?? title ?? "Milestones"}>            {for [i, m] in enumerate(items) => {                let st = m.status ?? "coming"                <li key={i} class={`ui-milestone-list-item ui-milestone-list-${st}`} aria-current={st == "current" ? "step" : null}>                    <span.ui-milestone-list-mark aria-hidden="true">                        {st == "done" && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-7"/></svg>}                        {st == "late" && "!"}                    </span>                    <div.ui-milestone-list-body>                        <div.ui-milestone-list-row>                            <h4.ui-milestone-list-name>{m.href != null ? <a href={m.href}>{m.title}</a> : m.title}<span.ui-sr> ({st == "coming" ? "to come" : st})</span></h4>                            {m.date != null && <span.ui-milestone-list-when>{st == "late" ? "Was due " : (st == "done" ? "" : "Due ")}                                <time datetime={str(m.date)}>{milestone_list_date(m.date)}</time></span>}                        </div>                        {m.text != null && <p.ui-milestone-list-text>{m.text}</p>}                        {m.progress != null && st != "done" && <div.ui-milestone-list-progress>                            <div.ui-milestone-list-bar role="progressbar" aria-label={`${m.title}: progress`} aria-valuemin="0" aria-valuemax="100"                                 aria-valuenow={m.progress}><span.ui-milestone-list-fill style={`width: ${m.progress}%`}></span></div>                            <span aria-hidden="true">{m.progress}%</span>                        </div>}                    </div>                </li>            }}        </ol>    </div>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On MilestoneList: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.