Commerce

PaymentMethods

Ways to pay as radio cards with their marks: card (with the brands taken), PayPal, Apple Pay, Google Pay, bank, cash.

#commerce#checkout#forms#select#card#no-js

Demo

Preview
Pay with

Paying with card (on_change).

Other ways

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

Example

The code of the preview above.

demos/payment_methods.qwe
component PaymentMethodsDemoLive() {    state way = "card"    <div.demo-stack.demo-narrow>        <PaymentMethods name="payment-demo-live" value={way} on_change={(v) => { way = v }}                        options={[{ value: "card", brands: ["visa", "mastercard", "amex"] }, "paypal", "apple", "google"]}/>        <p.demo-note>Paying with {way} (on_change).</p>    </div>}view PaymentMethodsDemo() =    <div.demo-row style="align-items: flex-start; gap: 32px">        <PaymentMethodsDemoLive/>        <div.demo-narrow>            <PaymentMethods name="payment-demo-more" label="Other ways" required={true}                            options={["bank", "later", { value: "invoice", label: "Pay by invoice", icon: "🧾", hint: "Business accounts only" },                                      { value: "cash", disabled: true }]}/>        </div>    </div>

Source

ui/payment_methods.qwe
// PaymentMethods: the ways to pay, as cards to pick one from, each with its// mark: a card, PayPal, Apple Pay, Google Pay, a bank transfer, cash. They are// real radio buttons, so a plain <form> sends the chosen `value` as `name`, and// the arrow keys move between them.////   <PaymentMethods value="card" options={["card", "paypal", "apple", "google"]}/>//   <PaymentMethods options={[{ value: "card", brands: ["visa", "mastercard", "amex"] }, "bank",//                              { value: "invoice", label: "Pay by invoice", icon: "🧾", hint: "Business accounts" }]}/>//// options: the built-in names, or maps of { value, label?, icon?, hint?, brands?,// disabled? }. The built-in names and their words: "card" (Credit or debit// card), "paypal" (PayPal), "apple" (Apple Pay), "google" (Google Pay), "bank"// (Bank transfer), "cash" (Cash on delivery), "later" (Pay later), "crypto"// (Cryptocurrency); a map with one of them as its value takes its words and mark.// icon: one of those names, or any text or emoji. brands: card marks to show// after the label, from "visa", "mastercard", "amex", "discover", "jcb",// "unionpay". value: the one chosen. name ("payment"), label ("Pay with"),// required, disabled, error as for every field. on_change(value): inside one// of your components, given the value chosen; pass value from your state, so// the choice stays as the component redraws.import ui/theme.qwe use ui_clslet payment_methods_css = css`.ui-pay { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-pay-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-pay-list { display: grid; gap: 8px; }.ui-pay-opt { position: relative; display: grid; grid-template-columns: auto auto 1fr auto; gap: 12px; align-items: center; padding: 10px 14px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); cursor: pointer;  transition: border-color .15s, background .15s, box-shadow .15s; }.ui-pay-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-pay-dot { position: relative; width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--ui-strong); background: var(--ui-surface); transition: border-color .15s; }.ui-pay-dot::after { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--ui-accent); transform: scale(0); transition: transform .12s; }.ui-pay-opt:hover { border-color: var(--ui-strong); }.ui-pay-opt:has(input:checked) { border-color: var(--ui-accent); background: color-mix(in srgb, var(--ui-accent-soft) 60%, var(--ui-surface)); box-shadow: inset 0 0 0 1px var(--ui-accent); }.ui-pay-opt input:checked + .ui-pay-dot { border-color: var(--ui-accent); border-width: 1.5px; }.ui-pay-opt input:checked + .ui-pay-dot::after { transform: scale(1); }.ui-pay-opt:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-pay-opt:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-pay-icon { display: inline-grid; place-items: center; width: 40px; height: 28px; border: 1px solid var(--ui-border); border-radius: 5px; background: var(--ui-subtle); color: var(--ui-fg); font-size: 16px; }.ui-pay-icon svg { width: 22px; height: 22px; }.ui-pay-text { display: grid; gap: 1px; min-width: 0; }.ui-pay-name { font-weight: 500; }.ui-pay-hint { font-size: 13px; color: var(--ui-muted); }.ui-pay-brands { display: flex; gap: 4px; }.ui-pay-brand { display: inline-grid; place-items: center; height: 20px; padding: 0 5px; border: 1px solid var(--ui-border); border-radius: 3px;  background: var(--ui-surface); font: 700 9px/1 var(--ui-font); letter-spacing: .04em; color: var(--ui-muted); text-transform: uppercase; }.ui-pay-invalid .ui-pay-opt { border-color: var(--ui-danger); }.ui-pay-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-pay-opt, .ui-pay-dot, .ui-pay-dot::after { transition: none; } }`// The built-in ways: their words and the path of their mark (24x24, stroked).let payment_methods_known = {    card: { label: "Credit or debit card", d: "M3 6h18v12H3zM3 10h18M7 14h4" },    paypal: { label: "PayPal", d: "M7 20l2-14h5a4 4 0 0 1 0 8h-3l-1 6zm3-14l-1 6" },    apple: { label: "Apple Pay", d: "M12 6c1-2 3-2 3-2s0 2-1 3-3 1-3 1 0-1 1-2zm-4 5c0-2 2-3 3-3s2 1 3 1 2-1 3-1 3 1 3 3c0 4-3 9-5 9s-2-1-3-1-1 1-3 1-4-5-4-9z" },    google: { label: "Google Pay", d: "M12 11h8a8 8 0 1 1-2.3-5.7M12 11v3h5" },    bank: { label: "Bank transfer", d: "M3 9l9-5 9 5H3zM5 9v8m4-8v8m6-8v8m4-8v8M3 20h18" },    cash: { label: "Cash on delivery", d: "M2 7h20v10H2zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM5 10v4m14-4v4" },    later: { label: "Pay later", hint: "Pay in 30 days, or in 3 parts", d: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2" },    crypto: { label: "Cryptocurrency", d: "M9 4v16m2-16v16M8 8h6a2.5 2.5 0 0 1 0 5H8m0 0h7a2.5 2.5 0 0 1 0 5H8" },}let payment_methods_brands = { visa: "Visa", mastercard: "MC", amex: "Amex", discover: "Disc", jcb: "JCB", unionpay: "UnionPay" }view PaymentMethods(options: list = ["card", "paypal"], value: str? = null, name: str = "payment", label: str? = "Pay with",                    required: bool = false, disabled: bool = false, error: str? = null, on_change = null, class: str? = null) {    style global payment_methods_css    let id = `payment-methods-${name}`    <fieldset class={ui_cls(["ui-pay", error != null && "ui-pay-invalid", class])} id={id} disabled={disabled}              aria-describedby={error != null ? `${id}-error` : null}>        {label != null && <legend.ui-pay-legend>{label}</legend>}        <div.ui-pay-list>            {for [i, o] in enumerate(options) => {                let it = type_of(o) == "str" ? { value: o } : o                let known = get(payment_methods_known, get(it, "icon") ?? it.value)                let icon = get(it, "icon") ?? it.value                let text = get(it, "label") ?? known.label ?? it.value                let hint = get(it, "hint") ?? known.hint                let oid = `${id}-${i}`                <label.ui-pay-opt for={oid} key={it.value}>                    <input type="radio" id={oid} name={name} value={it.value} checked={it.value == value} required={required}                           disabled={get(it, "disabled") == true} @change={(e) => { on_change && on_change(e.value) }}/>                    <span.ui-pay-dot aria-hidden="true"></span>                    <span.ui-pay-icon aria-hidden="true">                        {known != null                            ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d={known.d}/></svg>                            : icon}                    </span>                    <span.ui-pay-text>                        <span.ui-pay-name>{text}</span>                        {hint != null && <span.ui-pay-hint>{hint}</span>}                    </span>                    {len(get(it, "brands") ?? []) > 0 && <span.ui-pay-brands>                        {for b in it.brands => <span.ui-pay-brand key={b} aria-label={get(payment_methods_brands, b) ?? b}>{get(payment_methods_brands, b) ?? b}</span>}                    </span>}                </label>            }}        </div>        {error != null && <p.ui-pay-error id={`${id}-error`} role="alert">{error}</p>}    </fieldset>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.