Forms

CheckboxCards

Any number of choices as cards with a title, a line of detail and a check, each a real checkbox a form sends.

#forms#input#card#select#no-js

Demo

Preview
Add-ons

Billed monthly with the plan.

Notify me by

Notifying by email.

Open on

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

Example

The code of the preview above.

demos/checkbox_cards.qwe
component CheckboxCardsDemoLive() {    state picked = ["email"]    <div.demo-stack>        <CheckboxCards name="cc-demo-notify" label="Notify me by" values={picked} columns={3} size="sm"                       options={[{ label: "Email", value: "email", icon: "✉️" }, { label: "Text message", value: "sms", icon: "💬" },                                 { label: "Push", value: "push", icon: "🔔" }]}                       on_change={(v, on) => { picked = on ? picked + [v] : picked | filter((x) => x != v) }}/>        <p.demo-note>{len(picked) == 0 ? "No channel picked." : `Notifying by ${join(picked, ", ")}.`}</p>    </div>}view CheckboxCardsDemo() =    <div.demo-stack>        <CheckboxCards name="cc-demo-addons" label="Add-ons" values={["backups"]} hint="Billed monthly with the plan."                       options={[{ label: "Backups", value: "backups", description: "Daily, kept for 30 days.", meta: "$5" },                                 { label: "Monitoring", value: "monitoring", description: "Alerts by email when a check fails.", meta: "$3" },                                 { label: "Priority support", value: "support", description: "Sold out this month.", meta: "$20", disabled: true }]}/>        <CheckboxCardsDemoLive/>        <CheckboxCards name="cc-demo-days" label="Open on" columns={1} required={true} error="Pick at least one day."                       options={["Saturday", "Sunday"]}/>    </div>

Source

ui/checkbox_cards.qwe
// CheckboxCards: several choices as cards, each with a title, a line of detail// and a check in the corner, for picking any number (add-ons, interests, the// channels to notify). Each card is a real checkbox, so a plain <form> sends// `name` once per card checked, and Space toggles the focused one.////   <CheckboxCards name="addons" label="Add-ons" values={["backups"]}//                  options={[{ label: "Backups", value: "backups", description: "Daily, kept 30 days", meta: "$5" },//                            { label: "Monitoring", value: "monitoring", description: "Alerts by email" },//                            { label: "Support", value: "support", description: "Sold out", disabled: true }]}/>//// 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. values: the ones checked 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 (at least one; the browser does not// check it, your route does), disabled and field (the ids' start, "checkbox-cards-"// + name by default) as for every field. size: "sm" or "md".// on_change(value, checked): inside one of your components, given the card's value// and whether it is now checked.import ui/theme.qwe use ui_clslet checkbox_cards_css = css`.ui-checkbox-cards { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-checkbox-cards-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-checkbox-cards-req { margin-left: 2px; color: var(--ui-danger); }.ui-checkbox-cards-list { display: grid; grid-template-columns: repeat(var(--ui-checkbox-cards-cols, auto-fill), minmax(min(200px, 100%), 1fr)); gap: 10px; }.ui-checkbox-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-checkbox-cards-card input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-checkbox-cards-card:hover { border-color: var(--ui-strong); }.ui-checkbox-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-checkbox-cards-card:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-checkbox-cards-card:has(input:checked):has(input:focus-visible) { box-shadow: inset 0 0 0 1px var(--ui-accent), var(--ui-focus); }.ui-checkbox-cards-card:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-checkbox-cards-card:has(input:disabled):hover { border-color: var(--ui-border); }.ui-checkbox-cards-box {  flex: none; position: relative; width: 18px; height: 18px; margin-top: 1px; border-radius: 4px;  border: 1px solid var(--ui-strong); background: var(--ui-surface); transition: background .15s, border-color .15s;}.ui-checkbox-cards-box::after {  content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 10px; border: solid var(--ui-accent-fg);  border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0); transition: transform .12s;}.ui-checkbox-cards-card input:checked ~ .ui-checkbox-cards-box { background: var(--ui-accent); border-color: var(--ui-accent); }.ui-checkbox-cards-card input:checked ~ .ui-checkbox-cards-box::after { transform: rotate(45deg) scale(1); }.ui-checkbox-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-checkbox-cards-icon svg { width: 18px; height: 18px; }.ui-checkbox-cards-text { display: grid; flex: 1; gap: 2px; min-width: 0; }.ui-checkbox-cards-title { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-weight: 500; }.ui-checkbox-cards-meta { flex: none; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-checkbox-cards-desc { color: var(--ui-muted); font-size: 13px; }.ui-checkbox-cards-sm .ui-checkbox-cards-card { padding: 9px 11px; gap: 10px; }.ui-checkbox-cards-sm .ui-checkbox-cards-title { font-size: 13px; }.ui-checkbox-cards-sm .ui-checkbox-cards-desc, .ui-checkbox-cards-sm .ui-checkbox-cards-meta { font-size: 12px; }.ui-checkbox-cards-invalid .ui-checkbox-cards-card { border-color: var(--ui-danger); }.ui-checkbox-cards-hint, .ui-checkbox-cards-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-checkbox-cards-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-checkbox-cards-card, .ui-checkbox-cards-box, .ui-checkbox-cards-box::after { transition: none; } }`// An option as { label, value, description, meta, icon, disabled }, from a string or a map.fn checkbox_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 CheckboxCards(name: str, label: str? = null, options: list = [], values: list = [], 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 checkbox_cards_css    let id = field ?? `checkbox-cards-${name}`    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`])    let chosen = values | map((v) => str(v))    <fieldset class={ui_cls(["ui-checkbox-cards", size == "sm" && "ui-checkbox-cards-sm", error != null && "ui-checkbox-cards-invalid", class])}              id={id} disabled={disabled} aria-required={required ? "true" : null} aria-invalid={error != null ? "true" : null}              aria-describedby={described != "" ? described : null}>        {label != null && <legend.ui-checkbox-cards-legend>            {label}{required && <span.ui-checkbox-cards-req aria-hidden="true">*</span>}        </legend>}        <div.ui-checkbox-cards-list style={columns != null ? `--ui-checkbox-cards-cols: ${columns}` : null}>            {for [i, o] in enumerate(options) => {                let it = checkbox_cards_option(o)                let oid = `${id}-${i}`                <label.ui-checkbox-cards-card for={oid}>                    <input type="checkbox" id={oid} name={name} value={it.value} checked={contains(chosen, it.value)}                           disabled={it.disabled} aria-describedby={it.description != null ? `${oid}-desc` : null}                           @change={(e) => { on_change && on_change(it.value, e.checked) }}/>                    {it.icon != null && <span.ui-checkbox-cards-icon aria-hidden="true">{it.icon}</span>}                    <span.ui-checkbox-cards-text>                        <span.ui-checkbox-cards-title>                            <span>{it.label}</span>                            {it.meta != null && <span.ui-checkbox-cards-meta>{it.meta}</span>}                        </span>                        {it.description != null && <span.ui-checkbox-cards-desc id={`${oid}-desc`}>{it.description}</span>}                    </span>                    <span.ui-checkbox-cards-box aria-hidden="true"></span>                </label>            }}        </div>        {hint != null && <p.ui-checkbox-cards-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-checkbox-cards-error id={`${id}-error`} role="alert">{error}</p>}    </fieldset>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.