Forms

SearchInput

A search box with a magnifier and a clear button; Escape clears, Enter submits its form.

#forms#input#search#interactive

Demo

Preview

Press Enter to search.

8 of 8: Middlemarch, Moby-Dick, Mrs Dalloway, Persuasion, The Leopard, The Master and Margarita, To the Lighthouse, War and Peace

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

Example

The code of the preview above.

demos/search_input.qwe
let search_input_demo_books = ["Middlemarch", "Moby-Dick", "Mrs Dalloway", "Persuasion", "The Leopard",    "The Master and Margarita", "To the Lighthouse", "War and Peace"]component SearchInputDemoLive() {    state q = ""    state sent = ""    let found = search_input_demo_books | filter((b) => contains(lower(b), lower(q)))    <div.demo-stack.demo-narrow>        <SearchInput name="book" label="Find a book" placeholder="Title" on_input={(t) => { q = t }}                     on_submit={(t) => { sent = t }}/>        <p.demo-note>{len(found)} of {len(search_input_demo_books)}: {join(found, ", ")}</p>        {sent != "" && <p.demo-note>Enter pressed with “{sent}”.</p>}    </div>}view SearchInputDemo() =    <div.demo-stack>        <form.demo-narrow method="get" action="#search_input">            <SearchInput name="q" placeholder="Search the docs" value="components" hint="Press Enter to search."/>        </form>        <div.demo-row>            <SearchInput name="s" size="sm" placeholder="Small"/>            <SearchInput name="d" placeholder="Disabled" disabled={true}/>        </div>        <SearchInputDemoLive/>    </div>

Source

ui/search_input.qwe
// SearchInput: a search box with a magnifier, and a button that clears it once// something is typed (Escape clears it too). It is a real <input type="search">,// so in a plain <form method="get"> pressing Enter sends `name` ("q") as it is.////   <form action="/search"><SearchInput placeholder="Search the docs"/></form>//   <SearchInput label="Filter" on_input={(text) => { filter = text }}/>//// label: a visible label above it; without one, screen readers call it "Search".// value: the text to start with. hint, error, required, disabled, size ("sm" or// "md"), field (the input's id, "search-input-" + name by default) as for every field.// on_input(text): called with the text as it is typed, and with "" when it is cleared.// on_submit(text): called on Enter; with it, Enter no longer submits the form.// Both work when the SearchInput is inside one of your components.import ui/theme.qwe use ui_clslet search_input_css = css`.ui-search-input { display: grid; gap: 6px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-search-input-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-search-input-req { margin-left: 2px; color: var(--ui-danger); }.ui-search-input-hint { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-search-input-error { margin: 0; font-size: 13px; color: var(--ui-danger); }.ui-search-input-box { position: relative; display: flex; align-items: center; }.ui-search-input-icon { position: absolute; left: 11px; width: 16px; height: 16px; color: var(--ui-muted); pointer-events: none; }.ui-search-input-control {  box-sizing: border-box; width: 100%; height: 38px; margin: 0; padding: 0 36px 0 35px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  color: var(--ui-fg); font: 14px/1 var(--ui-font); appearance: none; transition: border-color .15s, box-shadow .15s;}.ui-search-input-control::placeholder { color: var(--ui-muted); opacity: 1; }.ui-search-input-control::-webkit-search-cancel-button, .ui-search-input-control::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }.ui-search-input-control:hover { border-color: var(--ui-strong); }.ui-search-input-control:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-search-input-control[aria-invalid="true"] { border-color: var(--ui-danger); }.ui-search-input-control:disabled { opacity: .55; cursor: not-allowed; }.ui-search-input-clear {  position: absolute; right: 5px; display: inline-flex; align-items: center; justify-content: center;  width: 28px; height: 28px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent;  color: var(--ui-muted); cursor: pointer;}.ui-search-input-clear svg { width: 14px; height: 14px; }.ui-search-input-clear:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-search-input-clear:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-search-input-sm .ui-search-input-control { height: 30px; padding-left: 31px; font-size: 13px; }.ui-search-input-sm .ui-search-input-icon { left: 9px; width: 14px; height: 14px; }.ui-search-input-sm .ui-search-input-clear { right: 3px; width: 24px; height: 24px; }@media (prefers-reduced-motion: reduce) { .ui-search-input-control { transition: none; } }`component SearchInput(name: str = "q", label: str? = null, value: str = "", placeholder: str? = null,                      hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false,                      field: str? = null, size: str = "md", on_input = null, on_submit = null, class: str? = null) {    style global search_input_css    state text = value    let fid = field ?? `search-input-${name}`    let described = ui_cls([hint != null && `${fid}-hint`, error != null && `${fid}-error`])    let clear = (e) => {        e.target.closest(".ui-search-input-box").querySelector("input").focus()        text = ""        on_input && on_input("")    }    <div class={ui_cls(["ui-search-input", size == "sm" && "ui-search-input-sm", class])}>        {label != null && <label.ui-search-input-label for={fid}>            {label}{required && <span.ui-search-input-req aria-hidden="true">*</span>}        </label>}        <div.ui-search-input-box>            <svg.ui-search-input-icon viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"                 stroke-linecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.75"/><path d="M10.5 10.5L14 14"/></svg>            <input.ui-search-input-control type="search" id={fid} name={name} value={text} placeholder={placeholder}                   aria-label={label == null ? "Search" : null} required={required} disabled={disabled}                   aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}                   autocomplete="off"                   @input={(e) => {                       text = e.value                       on_input && on_input(text)                   }}                   @keydown={(e) => {                       if e.key == "Escape" && text != "" {                           e.preventDefault()                           text = ""                           on_input && on_input("")                       }                       if e.key == "Enter" && on_submit != null {                           e.preventDefault()                           on_submit(text)                       }                   }}/>            {text != "" && !disabled && <button.ui-search-input-clear type="button" aria-label="Clear search" @click={clear}>                <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"                     aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>            </button>}        </div>        {hint != null && <p.ui-search-input-hint id={`${fid}-hint`}>{hint}</p>}        {error != null && <p.ui-search-input-error id={`${fid}-error`} role="alert">{error}</p>}    </div>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.