RangeSlider
Two thumbs on one track for a range (a price from–to), each with the keyboard; they cannot cross.
Demo
Showing people aged 25 to 40.
Not searched yet.
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/range_slider.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/range_slider.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/range_slider.qwe
component RangeSliderDemoLive() { state ages = [25, 40] state searched = [] <div.demo-stack> <RangeSlider name="rs-demo-age" label="Age" min={18} max={99} low={25} high={40} gap={1} on_change={(lo, hi) => { ages = [lo, hi] }} on_commit={(lo, hi) => { searched = [lo, hi] }}/> <p.demo-note>Showing people aged {ages[0]} to {ages[1]}.</p> <p.demo-note>{len(searched) == 0 ? "Not searched yet." : `Searched for ${searched[0]} to ${searched[1]}, once let go.`}</p> </div>}view RangeSliderDemo() = <div.demo-stack.demo-narrow> <RangeSlider name="rs-demo-price" label="Price" min={0} max={500} step={10} low={40} high={250} prefix="$" hint="Drag either end, or Tab to it and use the arrow keys."/> <RangeSliderDemoLive/> <RangeSlider name="rs-demo-km" label="Distance" min={0} max={50} step={0.5} low={2} high={10.5} format=" km"/> <RangeSlider name="rs-demo-off" label="Locked" low={20} high={80} format="%" disabled={true}/> </div>Source
ui/range_slider.qwe
// RangeSlider: a range between two values on one track, such as a price from// $40 to $250. Each thumb is the browser's own <input type="range">, so each works// with the arrow keys, PageUp / PageDown, Home and End and with screen readers,// and a plain <form> sends `name`_min and `name`_max. A thumb stops at the other// one: they cannot cross.//// <RangeSlider name="price" label="Price" min={0} max={500} step={10} low={40} high={250} prefix="$"/>// <RangeSlider name="age" label="Age" min={18} max={99} low={25} high={40} gap={1}// on_change={(lo, hi) => { ages = [lo, hi] }}/>//// min, max, step: as on a range input (0, 100, 1). low, high: where the thumbs start// (min and max by default). gap: the least distance between them (0).// prefix / format: text before / after each number shown ("$", "%", " km").// names: the two names sent, [min name, max name] (`name`_min and `name`_max).// show_value: false hides the numbers. hint, disabled and field (the ids' start,// "range-slider-" + name by default) as for every field.// on_change(low, high): inside one of your components, given both numbers, as a// thumb moves. on_commit(low, high): the same, once a thumb is let go (the end of a// drag, or a key press): for what costs a request, such as a search.import ui/theme.qwe use ui_clslet range_slider_css = css`.ui-range-slider { display: grid; gap: 6px; min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-range-slider-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }.ui-range-slider-label { padding: 0; font-size: 13px; font-weight: 500; }.ui-range-slider-value { font-size: 13px; color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-range-slider-track { position: relative; height: 22px; margin: 0 2px; }.ui-range-slider-track::before, .ui-range-slider-fill { content: ""; position: absolute; top: 8px; height: 6px; border-radius: 999px;}.ui-range-slider-track::before { left: 0; right: 0; background: var(--ui-subtle); box-shadow: inset 0 0 0 1px var(--ui-border); }.ui-range-slider-fill { left: var(--lo); right: calc(100% - var(--hi)); background: var(--ui-accent); }.ui-range-slider input { position: absolute; inset: 0; width: 100%; height: 22px; margin: 0; appearance: none; background: transparent; pointer-events: none;}.ui-range-slider input:focus { outline: none; }.ui-range-slider input::-webkit-slider-runnable-track { height: 22px; background: transparent; }.ui-range-slider input::-moz-range-track { background: transparent; }.ui-range-slider input::-webkit-slider-thumb { appearance: none; pointer-events: auto; width: 18px; height: 18px; margin-top: 2px; border-radius: 50%; cursor: grab; background: var(--ui-surface); border: 2px solid var(--ui-accent); box-shadow: var(--ui-shadow); transition: transform .12s, box-shadow .15s;}.ui-range-slider input::-moz-range-thumb { pointer-events: auto; width: 14px; height: 14px; border-radius: 50%; cursor: grab; background: var(--ui-surface); border: 2px solid var(--ui-accent); box-shadow: var(--ui-shadow);}.ui-range-slider input:active::-webkit-slider-thumb { cursor: grabbing; transform: scale(1.1); }.ui-range-slider input:focus-visible::-webkit-slider-thumb { box-shadow: var(--ui-focus); }.ui-range-slider input:focus-visible::-moz-range-thumb { box-shadow: var(--ui-focus); }.ui-range-slider-top { z-index: 1; }.ui-range-slider-scale { display: flex; justify-content: space-between; font-size: 12px; color: var(--ui-muted); }.ui-range-slider-hint { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-range-slider:disabled { opacity: .55; }.ui-range-slider:disabled input::-webkit-slider-thumb { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-range-slider input::-webkit-slider-thumb { transition: none; } }`// Where n is along min..max, as a CSS percentage.fn range_slider_at(n: num, lo: num, hi: num) -> str = hi > lo ? `${round((n - lo) * 100 / (hi - lo), 3)}%` : "0%"component RangeSlider(name: str? = null, label: str? = null, min: num = 0, max: num = 100, step: num = 1, low: num? = null, high: num? = null, gap: num = 0, prefix: str = "", format: str = "", names: list? = null, show_value: bool = true, hint: str? = null, disabled: bool = false, field: str? = null, on_change = null, on_commit = null, class: str? = null) { style global range_slider_css state a = low ?? min state b = high ?? max state top = "b" let id = field ?? `range-slider-${name ?? re.replace(lower(label ?? "range"), "[^a-z0-9]+", "-")}` let sent = names ?? (name != null ? [`${name}_min`, `${name}_max`] : [null, null]) let show = (n: num) => `${prefix}${n}${format}` let what = label ?? "Range" let moved = (e, which: str) => { let v = float(e.value) if which == "a" { let kept = v > b - gap ? b - gap : v if kept != v { e.target.value = kept } a = kept } else { let kept = v < a + gap ? a + gap : v if kept != v { e.target.value = kept } b = kept } // The thumb last moved stays on top, so two thumbs at one end can still be parted. top = which on_change && on_change(a, b) } <fieldset class={ui_cls(["ui-range-slider", class])} id={id} disabled={disabled} aria-labelledby={label != null ? `${id}-label` : null} aria-describedby={hint != null ? `${id}-hint` : null}> {(label != null || show_value) && <div.ui-range-slider-head> {label != null && <span.ui-range-slider-label id={`${id}-label`}>{label}</span>} {show_value && <output.ui-range-slider-value id={`${id}-out`} for={`${id}-min ${id}-max`} aria-live="polite"> {show(a)} – {show(b)} </output>} </div>} <div.ui-range-slider-track style={`--lo: ${range_slider_at(a, min, max)}; --hi: ${range_slider_at(b, min, max)}`}> <span.ui-range-slider-fill aria-hidden="true"></span> <input type="range" id={`${id}-min`} name={sent[0]} min={min} max={max} step={step} value={a} class={top == "a" ? "ui-range-slider-top" : null} aria-label={`${what}, lowest`} aria-valuetext={show(a)} @input={(e) => moved(e, "a")} @change={() => { on_commit && on_commit(a, b) }}/> <input type="range" id={`${id}-max`} name={sent[1]} min={min} max={max} step={step} value={b} class={top == "b" ? "ui-range-slider-top" : null} aria-label={`${what}, highest`} aria-valuetext={show(b)} @input={(e) => moved(e, "b")} @change={() => { on_commit && on_commit(a, b) }}/> </div> <div.ui-range-slider-scale aria-hidden="true"><span>{show(min)}</span><span>{show(max)}</span></div> {hint != null && <p.ui-range-slider-hint id={`${id}-hint`}>{hint}</p>} </fieldset>}Related
AddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloadsCharacterCounterA text box or textarea with a live 120 / 280 count that warns near the limit, as a hard stop or a soft one that blocks sending.5 views · 6 downloadsCheckboxA checkbox drawn by CSS on a real input, with an indeterminate state, and a group of them in a fieldset.5 views · 6 downloadsCheckboxCardsAny 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 downloads
Thread
Thread 0 posts
On RangeSlider: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.