Forms

ColorSwatches

Pick one colour from a set of round swatches: real radio buttons, so they post with a form and work with the keyboard.

#forms#picker#input#theme#no-js

Demo

Preview
Label colour

The arrow keys move between them.

Theme

Theme: ocean.

Small

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

Example

The code of the preview above.

demos/color_swatches.qwe
component ColorSwatchesDemoLive() {    state theme = "ocean"    <div.demo-stack>        <ColorSwatches name="theme" label="Theme" value={theme} size="lg" names={true} field="color-swatches-demo-live"                       options={[{ label: "Ocean", value: "ocean", color: "#0b6bcb" }, { label: "Forest", value: "forest", color: "#18794e" },                                 { label: "Plum", value: "plum", color: "#5b4bdb" }, { label: "Ember", value: "ember", color: "#c62a2f" },                                 { label: "Sand", value: "sand", color: "#d9b26f", disabled: true }]}                       on_change={(v) => { theme = v }}/>        <p.demo-note>Theme: {theme}.</p>    </div>}view ColorSwatchesDemo() =    <div.demo-stack>        <ColorSwatches name="color" label="Label colour" value="#18794e" field="color-swatches-demo-hex"                       options={["#5b4bdb", "#18794e", "#c62a2f", "#a35200", "#0b6bcb", "#17161f"]}                       hint="The arrow keys move between them."/>        <ColorSwatchesDemoLive/>        <div.demo-row>            <ColorSwatches name="tiny" label="Small" size="sm" options={["#5b4bdb", "#18794e", "#c62a2f"]} required={true}                           error="Pick a colour."/>        </div>    </div>

Source

ui/color_swatches.qwe
// ColorSwatches: pick one colour from a set, shown as round swatches. They are// real radio buttons in a <fieldset>, so a plain <form> sends the chosen value as// `name`, the arrow keys move between them, and each has a name for screen readers.////   <ColorSwatches name="color" label="Colour" value="#5b4bdb" options={["#5b4bdb", "#18794e", "#c62a2f"]}/>//   <ColorSwatches name="theme" label="Theme" size="lg" on_change={(v) => { theme = v }}//                  options={[{ label: "Ocean", value: "ocean", color: "#0b6bcb" },//                            { label: "Forest", value: "forest", color: "#18794e", disabled: true }]}/>//// options: colours as strings (the colour is the value and its name), or maps of// { label, value, color?, disabled? } (color: what is shown; the value by default).// value: the chosen one. size: "sm", "md" (default) or "lg". names (false): the// label written under each swatch. hint, error, required, disabled and field (the// ids' start, default "color-swatches-" + name) as in RadioGroup.// on_change(value): inside one of your components, given the value chosen.import ui/theme.qwe use ui_clslet color_swatches_css = css`.ui-color-swatches { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-color-swatches-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-color-swatches-req { margin-left: 2px; color: var(--ui-danger); }.ui-color-swatches-list { display: flex; flex-wrap: wrap; gap: 10px; }.ui-color-swatches-opt { position: relative; display: grid; justify-items: center; gap: 4px; cursor: pointer; }.ui-color-swatches-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-color-swatches-chip {  display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--c);  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-fg) 18%, transparent); transition: transform .12s, box-shadow .15s;}.ui-color-swatches-chip svg { width: 14px; height: 14px; opacity: 0; color: #fff; filter: drop-shadow(0 0 1px rgba(0, 0, 0, .7)); transition: opacity .12s; }.ui-color-swatches-opt:hover .ui-color-swatches-chip { transform: scale(1.08); }.ui-color-swatches-opt input:checked + .ui-color-swatches-chip { box-shadow: 0 0 0 2px var(--ui-bg), 0 0 0 4px var(--ui-accent); }.ui-color-swatches-opt input:checked + .ui-color-swatches-chip svg { opacity: 1; }.ui-color-swatches-opt input:focus-visible + .ui-color-swatches-chip { box-shadow: 0 0 0 2px var(--ui-bg), 0 0 0 4px var(--ui-accent), var(--ui-focus); }.ui-color-swatches-opt:has(input:disabled) { opacity: .4; cursor: not-allowed; }.ui-color-swatches-opt:has(input:disabled):hover .ui-color-swatches-chip { transform: none; }.ui-color-swatches-opt:has(input:disabled) .ui-color-swatches-chip::after {  content: ""; width: 30px; height: 2px; background: var(--ui-fg); transform: rotate(-45deg); border-radius: 1px;}.ui-color-swatches-name { font-size: 12px; color: var(--ui-muted); max-width: 72px; text-align: center; }.ui-color-swatches-opt input:checked ~ .ui-color-swatches-name { color: var(--ui-fg); font-weight: 500; }.ui-color-swatches-sm .ui-color-swatches-chip { width: 20px; height: 20px; }.ui-color-swatches-sm .ui-color-swatches-chip svg { width: 10px; height: 10px; }.ui-color-swatches-lg .ui-color-swatches-chip { width: 38px; height: 38px; }.ui-color-swatches-lg .ui-color-swatches-chip svg { width: 18px; height: 18px; }.ui-color-swatches-invalid .ui-color-swatches-chip { box-shadow: 0 0 0 2px var(--ui-danger); }.ui-color-swatches-hint, .ui-color-swatches-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-color-swatches-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-color-swatches-chip, .ui-color-swatches-chip svg { transition: none; } }`// An option as { label, value, color, disabled }, from a string or a map.fn color_swatches_option(o) = type_of(o) == "map"    ? { label: o.label ?? str(o.value), value: str(o.value ?? o.label), color: o.color ?? str(o.value ?? o.label), disabled: o.disabled ?? false }    : { label: str(o), value: str(o), color: str(o), disabled: false }// Only what a CSS colour can be made of: a word, #hex, or rgb(...) / hsl(...) with digits.fn color_swatches_safe(c: str) -> str = re.test(c, "^[#a-zA-Z0-9(),.% -]{1,60}$") ? c : "transparent"view ColorSwatches(name: str, label: str? = null, value: str? = null, options: list = [], size: str = "md",                   names: bool = false, hint: str? = null, error: str? = null, required: bool = false,                   disabled: bool = false, field: str? = null, on_change = null, class: str? = null) {    style global color_swatches_css    let id = field ?? `color-swatches-${name}`    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`])    <fieldset class={ui_cls(["ui-color-swatches", size != "md" && `ui-color-swatches-${size}`, error != null && "ui-color-swatches-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-color-swatches-legend>            {label}{required && <span.ui-color-swatches-req aria-hidden="true">*</span>}        </legend>}        <div.ui-color-swatches-list>            {for [i, o] in enumerate(options) => {                let it = color_swatches_option(o)                let oid = `${id}-${i}`                <label.ui-color-swatches-opt for={oid} title={it.label}>                    <input type="radio" id={oid} name={name} value={it.value} checked={it.value == value}                           required={required} disabled={it.disabled}                           @change={(e) => { on_change && on_change(e.value) }}/>                    <span.ui-color-swatches-chip style={`--c: ${color_swatches_safe(it.color)}`} aria-hidden="true">                        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2"                             stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-7"/></svg>                    </span>                    {names ? <span.ui-color-swatches-name>{it.label}</span> : <span.ui-sr>{it.label}</span>}                </label>            }}        </div>        {hint != null && <p.ui-color-swatches-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-color-swatches-error id={`${id}-error`} role="alert">{error}</p>}    </fieldset>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.