Commerce

ShippingOptions

Shipping methods as radio cards, each with its price and how long it takes; a plain form sends the choice.

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

Demo

Preview
Shipping

Sending by standard (on_change).

Delivery

Get it

It needs theme.qwe, as every component does, and price.qwe; 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/price.qwe$ curl -O https://quillolang.org/components/ui/shipping_options.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/shipping_options.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/shipping_options.qwe
let shipping_options_demo_ways = [    { value: "standard", label: "Standard", time: "3–5 business days", price: 0 },    { value: "express", label: "Express", time: "1–2 business days", price: 12.9 },    { value: "overnight", label: "Overnight", time: "Next business day, by 10 am", price: 29, hint: "Order by 4 pm" },    { value: "pickup", label: "Collect in store", time: "Ready tomorrow", price: 0, hint: "Oxford Street", disabled: true },]component ShippingOptionsDemoLive() {    state way = "standard"    <div.demo-stack.demo-narrow>        <ShippingOptions name="shipping-demo-live" value={way} options={shipping_options_demo_ways} on_change={(v) => { way = v }}/>        <p.demo-note>Sending by {way} (on_change).</p>    </div>}view ShippingOptionsDemo() =    <div.demo-row style="align-items: flex-start; gap: 32px">        <ShippingOptionsDemoLive/>        <div.demo-narrow>            <ShippingOptions name="shipping-demo-eur" label="Delivery" currency="EUR" locale="de-DE" required={true} error="Pick a delivery option"                             options={[{ value: "post", label: "Post", time: "4–6 days", price: 4.95 }, { value: "courier", label: "Courier", time: "1–2 days", price: 14.5 }]}/>        </div>    </div>

Source

ui/shipping_options.qwe
// ShippingOptions: the ways an order can be sent, as cards to pick one from,// each with its price and how long it takes. They are real radio buttons, so a// plain <form> sends the chosen `value` as `name`, and the arrow keys move// between them. Prices are written as Price writes them (this file needs price.qwe).////   <ShippingOptions value="standard" options={[//       { value: "standard", label: "Standard", time: "3–5 business days", price: 0 },//       { value: "express", label: "Express", time: "1–2 business days", price: 12.9 },//       { value: "pickup", label: "Collect in store", time: "Ready tomorrow", price: 0, hint: "Oxford Street" }]}/>//// options: a list of { value, label, price?, time?, hint?, disabled? }: price a// number (0 reads "Free") or text; time how long it takes; hint a quieter line.// value: the one chosen. name: the radios' name ("shipping"). label: the// group's legend ("Shipping"). currency, locale, cents as for Price. 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/price.qwe use price_textimport ui/theme.qwe use ui_clslet shipping_options_css = css`.ui-shipping { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-shipping-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-shipping-list { display: grid; gap: 8px; }.ui-shipping-opt { position: relative; display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; padding: 12px 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-shipping-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-shipping-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-shipping-dot::after { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--ui-accent); transform: scale(0); transition: transform .12s; }.ui-shipping-opt:hover { border-color: var(--ui-strong); }.ui-shipping-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-shipping-opt input:checked + .ui-shipping-dot { border-color: var(--ui-accent); border-width: 1.5px; }.ui-shipping-opt input:checked + .ui-shipping-dot::after { transform: scale(1); }.ui-shipping-opt:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-shipping-opt:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-shipping-text { display: grid; gap: 1px; min-width: 0; }.ui-shipping-name { font-weight: 500; }.ui-shipping-time, .ui-shipping-hint { font-size: 13px; color: var(--ui-muted); }.ui-shipping-price { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-shipping-free { color: var(--ui-success); }.ui-shipping-invalid .ui-shipping-opt { border-color: var(--ui-danger); }.ui-shipping-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-shipping-opt, .ui-shipping-dot, .ui-shipping-dot::after { transition: none; } }`view ShippingOptions(options: list = [], value: str? = null, name: str = "shipping", label: str? = "Shipping",                     currency: str = "USD", locale: str? = null, cents: bool = true, required: bool = false,                     disabled: bool = false, error: str? = null, on_change = null, class: str? = null) {    style global shipping_options_css    let id = `shipping-options-${name}`    <fieldset class={ui_cls(["ui-shipping", error != null && "ui-shipping-invalid", class])} id={id} disabled={disabled}              aria-describedby={error != null ? `${id}-error` : null}>        {label != null && <legend.ui-shipping-legend>{label}</legend>}        <div.ui-shipping-list>            {for [i, o] in enumerate(options) => {                let free = get(o, "price") != null && type_of(o.price) != "str" && dec(o.price) == 0                let oid = `${id}-${i}`                <label.ui-shipping-opt for={oid} key={o.value}>                    <input type="radio" id={oid} name={name} value={o.value} checked={o.value == value} required={required}                           disabled={get(o, "disabled") == true} @change={(e) => { on_change && on_change(e.value) }}/>                    <span.ui-shipping-dot aria-hidden="true"></span>                    <span.ui-shipping-text>                        <span.ui-shipping-name>{o.label}</span>                        {get(o, "time") != null && <span.ui-shipping-time>{o.time}</span>}                        {get(o, "hint") != null && <span.ui-shipping-hint>{o.hint}</span>}                    </span>                    {get(o, "price") != null && <span class={ui_cls(["ui-shipping-price", free && "ui-shipping-free"])}>                        {free ? "Free" : price_text(o.price, currency, locale, cents)}                    </span>}                </label>            }}        </div>        {error != null && <p.ui-shipping-error id={`${id}-error`} role="alert">{error}</p>}    </fieldset>}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.