Navigation

ProgressNav

Numbered steps as links: the ones done link back, this one is marked, and the ones to come wait their turn.

#navigation#progress#link#checkout#no-js

Demo

Preview

Step 2 of 4: Delivery. Every step is a link (ahead), and clicking one goes there.

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/progress_nav.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/progress_nav.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/progress_nav.qwe
let progress_nav_demo_steps = [    { label: "Cart", href: "#pn-cart", hint: "3 items" },    { label: "Delivery", href: "#pn-delivery", hint: "Lisbon" },    { label: "Payment", href: "#pn-payment" },    { label: "Review", href: "#pn-review" }]component ProgressNavDemoLive() {    state at = 1    <div.demo-stack>        <ProgressNav id="progress-nav-demo-live" steps={progress_nav_demo_steps} current={at} ahead={true}                     on_select={(i) => { at = i }}/>        <p.demo-note>Step {at + 1} of 4: {progress_nav_demo_steps[at].label}. Every step is a link (ahead), and clicking one goes there.</p>    </div>}view ProgressNavDemo() =    <div.demo-stack style="gap: 28px">        <ProgressNav id="progress-nav-demo" steps={progress_nav_demo_steps} current={2}/>        <ProgressNavDemoLive/>        <div style="max-width: 280px">            <ProgressNav vertical={true} current={4} label="Setup" steps={[                { label: "Create account", href: "#pn-1" }, { label: "Verify email", href: "#pn-2" },                { label: "Pick a plan", href: "#pn-3" }, { label: "Invite the team", href: "#pn-4" }            ]}/>        </div>    </div>

Source

ui/progress_nav.qwe
// ProgressNav: the steps of a process as numbered links: those done (with a check// mark, and a link back to them), this one, and those to come (not yet links). For// a checkout, a sign-up, a wizard whose steps are pages.////   <ProgressNav current={1} steps={[//       { label: "Cart", href: "/checkout/cart" },//       { label: "Delivery", href: "/checkout/delivery" },//       { label: "Payment", href: "/checkout/payment" },//       { label: "Review", href: "/checkout/review" }//   ]}/>//// steps: { label, href?, hint? }, in order. current: the index of this step, from 0;// those before it are done and link back (to go and change something), this one is// marked aria-current="step", and those after it are plain text, as they cannot be// reached yet. Set current to len(steps) when everything is done. ahead: let the// coming steps be links too (when the pages can be visited in any order).// vertical: one under another. label: what screen readers call it ("Progress"). id: the nav's id.// on_select(index): inside one of your components, called with the step clicked,// instead of following its link.import ui/theme.qwe use ui_clslet progress_nav_css = css`.ui-prognav ol { display: flex; margin: 0; padding: 0; list-style: none; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-prognav-item { position: relative; flex: 1 1 0; min-width: 0; display: flex; }.ui-prognav-item:not(:last-child)::after {  content: ""; position: absolute; top: 15px; left: calc(100% - 50% + 22px); right: calc(-50% + 22px); height: 2px;  border-radius: 2px; background: var(--ui-border);}.ui-prognav-done:not(:last-child)::after { background: var(--ui-accent); }.ui-prognav-step {  display: flex; flex: 1; flex-direction: column; align-items: center; gap: 8px; min-width: 0; padding: 0 4px 2px;  border-radius: var(--ui-radius-sm); color: var(--ui-muted); text-align: center; text-decoration: none;}a.ui-prognav-step:hover .ui-prognav-label { color: var(--ui-accent); text-decoration: underline; text-underline-offset: 3px; }.ui-prognav-step:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-prognav-mark {  position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px;  border-radius: 50%; border: 2px solid var(--ui-border); background: var(--ui-surface); color: var(--ui-muted);  font: 600 13px/1 var(--ui-font); font-variant-numeric: tabular-nums;}.ui-prognav-mark svg { width: 15px; height: 15px; }.ui-prognav-done .ui-prognav-mark { border-color: var(--ui-accent); background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-prognav-current .ui-prognav-mark { border-color: var(--ui-accent); color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-prognav-current .ui-prognav-step { color: var(--ui-fg); }.ui-prognav-label { font-weight: 500; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-prognav-current .ui-prognav-label { font-weight: 600; }.ui-prognav-hint { color: var(--ui-muted); font-size: 12px; }.ui-prognav-vertical ol { flex-direction: column; }.ui-prognav-vertical .ui-prognav-item { flex: none; }.ui-prognav-vertical .ui-prognav-item:not(:last-child)::after { top: 38px; bottom: -4px; left: 15px; right: auto; width: 2px; height: auto; }.ui-prognav-vertical .ui-prognav-step { flex-direction: row; align-items: flex-start; gap: 12px; padding: 0 0 20px; text-align: left; }.ui-prognav-vertical .ui-prognav-item:last-child .ui-prognav-step { padding-bottom: 0; }.ui-prognav-vertical .ui-prognav-mark { flex: none; }.ui-prognav-vertical .ui-prognav-text { padding-top: 6px; }@media (max-width: 600px) {  .ui-prognav:not(.ui-prognav-vertical) .ui-prognav-hint { display: none; }  .ui-prognav:not(.ui-prognav-vertical) .ui-prognav-label { font-size: 12px; }}`view ProgressNav(steps: list = [], current: int = 0, ahead: bool = false, vertical: bool = false,                 label: str = "Progress", id: str? = null, on_select = null, class: str? = null) {    style global progress_nav_css    <nav class={ui_cls(["ui-prognav", vertical && "ui-prognav-vertical", class])} id={id} aria-label={label}>        <ol>            {for [i, s] in enumerate(steps) => {                let phase = i < current ? "done" : i == current ? "current" : "coming"                let linked = get(s, "href") != null && (phase == "done" || (ahead && phase == "coming"))                let body = [                    <span.ui-prognav-mark aria-hidden="true">{phase == "done"                        ? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"                               stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-7"/></svg>                        : i + 1}</span>,                    <span.ui-prognav-text>                        <span.ui-prognav-label>{s.label}{phase == "done" && <span.ui-sr> (done)</span>}</span>                        {get(s, "hint") != null && <span.ui-prognav-hint>{s.hint}</span>}                    </span>,                ]                <li class={`ui-prognav-item ui-prognav-${phase}`}>                    {linked                        ? <a.ui-prognav-step href={s.href} aria-current={phase == "current" ? "step" : null}                             @click={(e) => { if on_select != null { e.preventDefault(); on_select(i) } }}>{body}</a>                        : <span.ui-prognav-step aria-current={phase == "current" ? "step" : null}>{body}</span>}                </li>            }}        </ol>    </nav>}
Thread
64Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.