LoanApplicationSteps
Where a loan application stands, from submitted to disbursed: the steps done with their dates, the current one, and what is needed next.
Demo
Personal loan, $24,000
Reference APP-2026-04817
- Submitted, CompletedOct 20, 2026
- Documents, Current stepSince Oct 21, 2026
- Credit check, Not startedExpected Oct 30, 2026
- Approval, Not startedExpected Nov 4, 2026
- Disbursement, Not startedExpected Nov 6, 2026
What we need next (2 to go)
We need two more documents before we can run the credit check.
- Photo ID, received
- Last two payslips, needed PDF or a photo, each page readable
- Proof of address, needed A utility bill from the last three months
Car loan, €15,000
Reference APP-2026-03321
- Submitted, Completed2 Oct 2026
- Documents, Completed3 Oct 2026
- Credit check, Completed7 Oct 2026
- Approval, DeclinedDeclined 9 Oct 2026
- Disbursement, CancelledWill not happen
Why it was declined
Your monthly debts would be 52% of your income with this loan. A smaller amount or a longer term may be approved.
Try another amountLoan of $8,500
Reference APP-2026-02210
- Submitted, CompletedSep 14, 2026
- Documents, CompletedSep 15, 2026
- Credit check, CompletedSep 16, 2026
- Approval, CompletedSep 18, 2026
- Disbursement, CompletedSep 19, 2026
All done
$8,500 was paid into your account ending 3390 on Sep 19.
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/loan_application_steps.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/loan_application_steps.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/loan_application_steps.qwe
view LoanApplicationStepsDemo() { <div.demo-stack> <LoanApplicationSteps id="loan-application-steps-demo-docs" reference="APP-2026-04817" product="Personal loan" amount="$24,000" current="documents" dates={{ submitted: "2026-10-20", documents: "2026-10-21" }} expected={{ credit_check: "2026-10-30", approval: "2026-11-04", disbursement: "2026-11-06" }} note="We need two more documents before we can run the credit check." needed={[{ label: "Photo ID", done: true }, { label: "Last two payslips", detail: "PDF or a photo, each page readable" }, { label: "Proof of address", detail: "A utility bill from the last three months" }]} action_href="#upload" action_label="Upload documents"/> <div.demo-row style="align-items: stretch"> <div style="flex: 1 1 320px; max-width: 420px"> <LoanApplicationSteps id="loan-application-steps-demo-declined" reference="APP-2026-03321" product="Car loan" amount="€15,000" current="approval" outcome="declined" locale="en-GB" dates={{ submitted: "2026-10-02", documents: "2026-10-03", credit_check: "2026-10-07", approval: "2026-10-09" }} note="Your monthly debts would be 52% of your income with this loan. A smaller amount or a longer term may be approved." action_href="#calculator" action_label="Try another amount"/> </div> <div style="flex: 1 1 320px; max-width: 420px"> <LoanApplicationSteps id="loan-application-steps-demo-done" reference="APP-2026-02210" amount="$8,500" outcome="complete" dates={{ submitted: "2026-09-14", documents: "2026-09-15", credit_check: "2026-09-16", approval: "2026-09-18", disbursement: "2026-09-19" }} note="$8,500 was paid into your account ending 3390 on Sep 19."/> </div> </div> </div>}Source
ui/loan_application_steps.qwe
// LoanApplicationSteps: where a loan application stands. Its steps (submitted,// documents, credit check, approval, disbursement) in a row, or a column when// narrow: those done with a tick and the day they were done, the current one// marked (aria-current="step") with what is happening, those ahead with the day// they are expected. Under them, what is needed next as a checklist, and a button// to do it.//// <LoanApplicationSteps reference="APP-2026-04817" amount="$24,000" product="Personal loan" current="documents"// dates={{ submitted: "2026-10-20" }} expected={{ approval: "2026-11-04" }}// note="We need two more documents before the credit check."// needed={[{ label: "Photo ID", done: true }, { label: "Last two payslips" }]}// action_href="/applications/04817/upload" action_label="Upload documents"/>// <LoanApplicationSteps reference="APP-2026-03321" current="approval" outcome="declined" note="Your debts are too high for this amount."/>//// current: the key of the step it is at. steps: the steps, a list of { key, label,// detail? } (detail is a line under a step); by default submitted, documents,// credit_check, approval and disbursement. dates: when each step was done, a map// of key to ISO date; expected: when steps ahead should be, the same way.// outcome: "declined" (the current step failed, in red, and the rest will not// happen), "complete" (every step done, the last included), or null while it goes// on. note: a line about the current step. needed: what is needed next, a list of// { label, done?, detail? } (or plain text). action_href / action_label: a button// under it ("Continue"). reference, product, amount: a line at the top (amount as// text, formatted). locale: how dates are written (the program's; en-US).// label: what screen readers call the steps ("Application progress"). id, class.import ui/theme.qwe use ui_clslet loan_application_steps_css = css`.ui-loan-steps { container-type: inline-size; display: grid; gap: 18px; min-width: 0; padding: 18px 20px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); --ui-loan-steps-on: var(--ui-accent); }.ui-loan-steps-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; }.ui-loan-steps-title { margin: 0; font-size: 16px; font-weight: 650; }.ui-loan-steps-ref { margin: 0; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-loan-steps-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }.ui-loan-steps-step { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 0 12px; padding-bottom: 18px; }.ui-loan-steps-step:last-child { padding-bottom: 0; }.ui-loan-steps-step::before { content: ""; position: absolute; top: 30px; bottom: 2px; left: 13px; width: 2px; border-radius: 1px; background: var(--ui-border); }.ui-loan-steps-step:last-child::before { display: none; }.ui-loan-steps-done::before { background: var(--ui-loan-steps-on); }.ui-loan-steps-dot { display: grid; place-items: center; width: 28px; height: 28px; border: 2px solid var(--ui-border); border-radius: 50%; background: var(--ui-surface); color: var(--ui-muted); font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }.ui-loan-steps-dot svg { width: 14px; height: 14px; }.ui-loan-steps-done .ui-loan-steps-dot { border-color: var(--ui-loan-steps-on); background: var(--ui-loan-steps-on); color: var(--ui-accent-fg); }.ui-loan-steps-current .ui-loan-steps-dot { border-color: var(--ui-loan-steps-on); color: var(--ui-loan-steps-on); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ui-loan-steps-on) 18%, transparent); }.ui-loan-steps-failed .ui-loan-steps-dot { border-color: var(--ui-danger); background: var(--ui-danger); color: #fff; box-shadow: none; }.ui-loan-steps-text { display: grid; gap: 1px; padding-top: 3px; min-width: 0; }.ui-loan-steps-label { font-weight: 600; }.ui-loan-steps-todo .ui-loan-steps-label, .ui-loan-steps-skipped .ui-loan-steps-label { color: var(--ui-muted); font-weight: 500; }.ui-loan-steps-skipped .ui-loan-steps-label { text-decoration: line-through; }.ui-loan-steps-when { color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-loan-steps-current .ui-loan-steps-when { color: var(--ui-loan-steps-on); font-weight: 600; }.ui-loan-steps-failed .ui-loan-steps-when { color: var(--ui-danger); }.ui-loan-steps-detail { color: var(--ui-muted); font-size: 13px; }@container (min-width: 640px) { .ui-loan-steps-list { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); } .ui-loan-steps-step { grid-template-columns: 1fr; justify-items: center; gap: 8px; padding: 0 6px; text-align: center; } .ui-loan-steps-step::before { top: 13px; bottom: auto; left: calc(50% + 20px); right: calc(-50% + 20px); width: auto; height: 2px; } .ui-loan-steps-text { padding-top: 0; }}.ui-loan-steps-next { display: grid; gap: 10px; padding: 14px 16px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-loan-steps-failed-note { background: var(--ui-danger-soft); }.ui-loan-steps-next h4 { margin: 0; font-size: 13px; font-weight: 650; }.ui-loan-steps-note { margin: 0; }.ui-loan-steps-needs { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-loan-steps-needs li { display: grid; grid-template-columns: 18px 1fr; gap: 8px; align-items: start; }.ui-loan-steps-check { display: grid; place-items: center; width: 18px; height: 18px; margin-top: 1px; border: 1.5px solid var(--ui-strong); border-radius: 4px; }.ui-loan-steps-check svg { width: 11px; height: 11px; }.ui-loan-steps-got .ui-loan-steps-check { border-color: var(--ui-success); background: var(--ui-success); color: #fff; }.ui-loan-steps-got .ui-loan-steps-need { color: var(--ui-muted); text-decoration: line-through; }.ui-loan-steps-need small { display: block; color: var(--ui-muted); font-size: 12px; text-decoration: none; }.ui-loan-steps-action { justify-self: start; display: inline-flex; align-items: center; height: 34px; padding: 0 14px; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font-weight: 600; text-decoration: none; }.ui-loan-steps-action:hover { filter: brightness(1.08); }.ui-loan-steps-action:focus-visible { outline: none; box-shadow: var(--ui-focus); }`let loan_application_steps_default = [ { key: "submitted", label: "Submitted" }, { key: "documents", label: "Documents" }, { key: "credit_check", label: "Credit check" }, { key: "approval", label: "Approval" }, { key: "disbursement", label: "Disbursement" },]fn loan_application_steps_tick() = <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7.5l2.6 2.6L11 4.5"/></svg>view LoanApplicationSteps(current: str? = null, steps: list = loan_application_steps_default, dates: map = {}, expected: map = {}, outcome: str? = null, note: str? = null, needed: list = [], action_href: str? = null, action_label: str = "Continue", reference: str? = null, product: str? = null, amount: str? = null, locale: str? = null, label: str = "Application progress", id: str? = null, class: str? = null) { style global loan_application_steps_css let at = outcome == "complete" ? len(steps) : (index_of(for s in steps => s.key, current) ?? 0) let at_i = at < 0 ? 0 : at let declined = outcome == "declined" let day = (d) => date.format(str(d), { locale: locale ?? "en-US", style: "medium" }) let items = for n in needed => type_of(n) == "str" ? { label: n } : n let left = len(filter(items, (n) => n.done != true)) let title = product != null ? (amount != null ? `${product}, ${amount}` : product) : (amount != null ? `Loan of ${amount}` : null) <section class={ui_cls(["ui-loan-steps", class])} id={id} aria-label={title ?? label}> {(title != null || reference != null) && <div.ui-loan-steps-head> {title != null && <h3.ui-loan-steps-title>{title}</h3>} {reference != null && <p.ui-loan-steps-ref>Reference {reference}</p>} </div>} <ol.ui-loan-steps-list aria-label={label}> {for [i, s] in enumerate(steps) { let phase = i < at_i ? "done" : i == at_i && outcome != "complete" ? (declined ? "failed" : "current") : (declined ? "skipped" : "todo") let done_on = get(dates, s.key, null) let due_on = get(expected, s.key, null) let when = phase == "done" ? (done_on != null ? day(done_on) : "Done") : phase == "failed" ? (done_on != null ? `Declined ${day(done_on)}` : "Declined") : phase == "current" ? (done_on != null ? `Since ${day(done_on)}` : "In progress") : phase == "skipped" ? "Will not happen" : (due_on != null ? `Expected ${day(due_on)}` : "Not started") let said = match phase { "done" => "Completed" "current" => "Current step" "failed" => "Declined" "skipped" => "Cancelled" _ => "Not started" } <li class={`ui-loan-steps-step ui-loan-steps-${phase}`} key={s.key} data-key={s.key} data-state={phase} aria-current={phase == "current" || phase == "failed" ? "step" : null}> <span.ui-loan-steps-dot aria-hidden="true">{phase == "done" ? loan_application_steps_tick() : (phase == "failed" ? "✕" : str(i + 1))}</span> <span.ui-loan-steps-text> <span.ui-loan-steps-label>{s.label}<span.ui-sr>, {said}</span></span> <span.ui-loan-steps-when>{when}</span> {s.detail != null && <span.ui-loan-steps-detail>{s.detail}</span>} </span> </li> }} </ol> {(note != null || len(items) > 0 || action_href != null) && <div class={ui_cls(["ui-loan-steps-next", declined && "ui-loan-steps-failed-note"])}> <h4>{declined ? "Why it was declined" : outcome == "complete" ? "All done" : len(items) > 0 ? (left > 0 ? `What we need next (${left} to go)` : "We have everything we need") : "What happens next"}</h4> {note != null && <p.ui-loan-steps-note>{note}</p>} {len(items) > 0 && <ul.ui-loan-steps-needs> {for n in items => <li class={n.done == true ? "ui-loan-steps-got" : null} key={n.label}> <span.ui-loan-steps-check aria-hidden="true">{n.done == true && loan_application_steps_tick()}</span> <span.ui-loan-steps-need>{n.label}<span.ui-sr>{n.done == true ? ", received" : ", needed"}</span>{" "} {n.detail != null && <small>{n.detail}</small>} </span> </li>} </ul>} {action_href != null && <a.ui-loan-steps-action href={action_href}>{action_label}</a>} </div>} </section>}Related
AmortizationTableA loan's payment schedule worked out from its amount, rate and term: each month's principal, interest and balance, grouped by year with the year's totals.7 views · 7 downloadsCollateralListWhat secures a loan: each item's valuation and documents, and the loan-to-value ratio as a meter against the lender's limit.7 views · 5 downloadsCreditScoreA credit score on a gauge of its bands from poor to excellent, the band it falls in, the change since the last report and the factors that move it.7 views · 8 downloadsDebtPayoffCompares avalanche and snowball on several debts with an extra payment: months, interest, payoff order, each month, and warns of a debt that never clears.3 views · 6 downloadsLoanCalculatorA loan's amount, rate and term as sliders and boxes, with the monthly payment, total interest and total paid following as they move.7 views · 5 downloadsLoanCommitteeQueueLoan applications before the credit committee: member, amount, purpose, the votes so far, where each stands, and approve or decline per row.7 views · 10 downloads
Thread
Thread 0 posts
On LoanApplicationSteps: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.