RadioCards
One choice of several as cards with a title, a line of detail and a dot, each a real radio a form sends.
Demo
Size M it is.
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/radio_cards.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/radio_cards.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/radio_cards.qwe
component RadioCardsDemoLive() { state size = "m" <div.demo-stack> <RadioCards name="rc-demo-size" label="Size" value={size} columns={4} size="sm" options={[{ label: "S", value: "s", description: "1 CPU" }, { label: "M", value: "m", description: "2 CPUs" }, { label: "L", value: "l", description: "4 CPUs" }, { label: "XL", value: "xl", description: "8 CPUs" }]} on_change={(v) => { size = v }}/> <p.demo-note>Size {upper(size)} it is.</p> </div>}view RadioCardsDemo() = <div.demo-stack> <RadioCards name="rc-demo-plan" label="Plan" value="pro" hint="Change it any time." options={[{ label: "Hobby", value: "hobby", description: "One project and community help.", meta: "Free" }, { label: "Pro", value: "pro", description: "Unlimited projects, email support.", meta: "$12/mo" }, { label: "Enterprise", value: "ent", description: "Single sign-on and a contract. Talk to us.", meta: "Custom", disabled: true }]}/> <RadioCardsDemoLive/> <RadioCards name="rc-demo-ship" label="Shipping" columns={1} required={true} error="Choose how to ship it." options={[{ label: "Standard", value: "std", description: "3 to 5 working days", meta: "Free", icon: "📦" }, { label: "Express", value: "exp", description: "Next working day", meta: "$9", icon: "🚀" }]}/> </div>Source
ui/radio_cards.qwe
// RadioCards: one choice of several as cards, each with a title, a line of// detail and a dot, for choices worth a sentence each (a plan, a shipping method,// a size of machine). Each card is a real radio, so a plain <form> sends the// chosen `value` as `name`, and the arrow keys move the choice.//// <RadioCards name="plan" label="Plan" value="pro"// options={[{ label: "Hobby", value: "hobby", description: "One project, community help", meta: "Free" },// { label: "Pro", value: "pro", description: "Unlimited projects, email support", meta: "$12/mo" },// { label: "Enterprise", value: "ent", description: "Talk to us", disabled: true }]}/>// <RadioCards name="size" label="Size" columns={4} options={["S", "M", "L", "XL"]} on_change={(v) => { size = v }}/>//// options: a list of strings, or of { label, value, description?, meta?, icon?,// disabled? }: meta is short text at the right of the title (a price); icon, HTML// or an emoji before it. value: the one chosen to start with. columns: how many// cards across (as many as fit, 200px each, by default); 1 stacks them.// label (the legend), hint, error, required, disabled and field (the ids' start,// "radio-cards-" + name by default) as for every field. size: "sm" or "md".// on_change(value): inside one of your components, given the value chosen.import ui/theme.qwe use ui_clslet radio_cards_css = css`.ui-radio-cards { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-radio-cards-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-radio-cards-req { margin-left: 2px; color: var(--ui-danger); }.ui-radio-cards-list { display: grid; grid-template-columns: repeat(var(--ui-radio-cards-cols, auto-fill), minmax(min(200px, 100%), 1fr)); gap: 10px; }.ui-radio-cards-card { position: relative; display: flex; align-items: flex-start; gap: 12px; min-width: 0; padding: 12px 14px; cursor: pointer; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); transition: border-color .15s, background .15s, box-shadow .15s;}.ui-radio-cards-card input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-radio-cards-card:hover { border-color: var(--ui-strong); }.ui-radio-cards-card: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-radio-cards-card:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-radio-cards-card:has(input:checked):has(input:focus-visible) { box-shadow: inset 0 0 0 1px var(--ui-accent), var(--ui-focus); }.ui-radio-cards-card:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-radio-cards-card:has(input:disabled):hover { border-color: var(--ui-border); }.ui-radio-cards-dot { flex: none; position: relative; width: 18px; height: 18px; margin-top: 1px; border-radius: 50%; border: 1px solid var(--ui-strong); background: var(--ui-surface); transition: border-color .15s;}.ui-radio-cards-dot::after { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--ui-accent); transform: scale(0); transition: transform .12s;}.ui-radio-cards-card input:checked ~ .ui-radio-cards-dot { border-color: var(--ui-accent); border-width: 1.5px; }.ui-radio-cards-card input:checked ~ .ui-radio-cards-dot::after { transform: scale(1); }.ui-radio-cards-icon { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); color: var(--ui-accent); font-size: 18px; }.ui-radio-cards-icon svg { width: 18px; height: 18px; }.ui-radio-cards-text { display: grid; flex: 1; gap: 2px; min-width: 0; }.ui-radio-cards-title { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-weight: 500; }.ui-radio-cards-meta { flex: none; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-radio-cards-desc { color: var(--ui-muted); font-size: 13px; }.ui-radio-cards-sm .ui-radio-cards-card { padding: 9px 11px; gap: 10px; }.ui-radio-cards-sm .ui-radio-cards-title { font-size: 13px; }.ui-radio-cards-sm .ui-radio-cards-desc, .ui-radio-cards-sm .ui-radio-cards-meta { font-size: 12px; }.ui-radio-cards-invalid .ui-radio-cards-card { border-color: var(--ui-danger); }.ui-radio-cards-hint, .ui-radio-cards-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-radio-cards-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-radio-cards-card, .ui-radio-cards-dot, .ui-radio-cards-dot::after { transition: none; } }`// An option as { label, value, description, meta, icon, disabled }, from a string or a map.fn radio_cards_option(o) = type_of(o) == "map" ? { label: o.label ?? str(o.value), value: str(o.value ?? o.label), description: o.description, meta: o.meta, icon: o.icon, disabled: o.disabled ?? false } : { label: str(o), value: str(o), description: null, meta: null, icon: null, disabled: false }view RadioCards(name: str, label: str? = null, options: list = [], value: str | num | null = null, columns: int? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_change = null, class: str? = null) { style global radio_cards_css let id = field ?? `radio-cards-${name}` let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let chosen = value != null ? str(value) : null <fieldset class={ui_cls(["ui-radio-cards", size == "sm" && "ui-radio-cards-sm", error != null && "ui-radio-cards-invalid", class])} id={id} disabled={disabled} role="radiogroup" aria-required={required ? "true" : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}> {label != null && <legend.ui-radio-cards-legend> {label}{required && <span.ui-radio-cards-req aria-hidden="true">*</span>} </legend>} <div.ui-radio-cards-list style={columns != null ? `--ui-radio-cards-cols: ${columns}` : null}> {for [i, o] in enumerate(options) => { let it = radio_cards_option(o) let oid = `${id}-${i}` <label.ui-radio-cards-card for={oid}> <input type="radio" id={oid} name={name} value={it.value} checked={it.value == chosen} required={required} disabled={it.disabled} aria-describedby={it.description != null ? `${oid}-desc` : null} @change={(e) => { on_change && on_change(e.value) }}/> {it.icon != null && <span.ui-radio-cards-icon aria-hidden="true">{it.icon}</span>} <span.ui-radio-cards-text> <span.ui-radio-cards-title> <span>{it.label}</span> {it.meta != null && <span.ui-radio-cards-meta>{it.meta}</span>} </span> {it.description != null && <span.ui-radio-cards-desc id={`${oid}-desc`}>{it.description}</span>} </span> <span.ui-radio-cards-dot aria-hidden="true"></span> </label> }} </div> {hint != null && <p.ui-radio-cards-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-radio-cards-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
CheckboxCardsAny number of choices as cards with a title, a line of detail and a check, each a real checkbox a form sends.5 views · 6 downloadsComboboxA text box that filters a list of options as you type, or asks for them (a server, a map service), with the arrow keys, Enter and Escape.5 views · 6 downloadsDependentSelectTwo drop-downs where the second's options depend on the first, both real selects a form sends.5 views · 7 downloadsPhoneInputA country code select and a number box, sent together in international form (+351912345678) with the country's ISO code.6 views · 6 downloadsRadioGroupOne choice of several: real radio buttons in a fieldset, in a column, a row or as cards.5 views · 6 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloads
Thread
Thread 0 posts
On RadioCards: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.