NavSearch
A search box for a navbar that grows when focused, with / to reach it and Escape to clear; a real form sends it.
Demo
Acme
Press / anywhere to search; the box grows when focused.
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/nav_search.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/nav_search.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/nav_search.qwe
component NavSearchDemoLive() { state q = "" state sent = null <div.demo-stack> <div style="display: flex; align-items: center; gap: 16px; height: 56px; padding: 0 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius)"> <b>Acme</b> <span style="flex: 1"></span> <NavSearch id="nav-search-demo" placeholder="Search docs" on_input={(t) => { q = t }} on_submit={(t) => { sent = t }}/> </div> <p.demo-note>{sent != null ? `Searched for "${sent}".` : q != "" ? `Typing: ${q}` : "Press / anywhere to search; the box grows when focused."}</p> </div>}view NavSearchDemo() = <div.demo-stack> <NavSearchDemoLive/> <div.demo-row> <NavSearch id="nav-search-demo-plain" action="#nav_search" shortcut={false} width={140} wide={240} value="quillo"/> <span.demo-note>A plain one: Enter sends q to its action.</span> </div> </div>Source
ui/nav_search.qwe
// NavSearch: a search box for a navbar: a magnifier and a short field that grows// wider when it is focused, and shrinks back when it is left empty. Pressing "/"// anywhere on the page (outside another field) focuses it; Escape clears it, then// leaves it. It is a real <input type="search"> in a <form method="get">, so Enter// sends `name` ("q") to `action` without any script.//// <NavSearch action="/search" placeholder="Search docs"/>// <NavSearch on_submit={(text) => { results = find(text) }} on_input={(text) => { query = text }}/>//// action: where the form goes ("/search"). name: the field's name ("q"). value: the// text to start with. placeholder ("Search"). width, wide: its width at rest and when// focused, in pixels (160, 280). shortcut: focus it with "/" (true); the key shows in// the box as a hint. label: its name for screen readers ("Search").// on_input(text): inside one of your components, called as the text changes.// on_submit(text): called on Enter; with it, the form is not sent.import ui/theme.qwe use ui_clslet nav_search_css = css`.ui-navsearch { position: relative; display: inline-flex; align-items: center; margin: 0; font: 14px/1 var(--ui-font); color: var(--ui-fg); }.ui-navsearch-icon { position: absolute; left: 10px; width: 16px; height: 16px; color: var(--ui-muted); pointer-events: none; }.ui-navsearch-input { box-sizing: border-box; width: var(--ui-navsearch-w, 160px); height: 36px; margin: 0; padding: 0 34px 0 32px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-subtle); color: var(--ui-fg); font: inherit; appearance: none; transition: width .2s ease-out, background .15s, border-color .15s, box-shadow .15s;}.ui-navsearch-input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-navsearch-input::-webkit-search-cancel-button, .ui-navsearch-input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }.ui-navsearch-input:hover { border-color: var(--ui-strong); }.ui-navsearch-input:focus, .ui-navsearch-open .ui-navsearch-input { outline: none; width: var(--ui-navsearch-wide, 280px); background: var(--ui-surface); border-color: var(--ui-accent); box-shadow: var(--ui-focus);}.ui-navsearch-key { position: absolute; right: 8px; display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 5px; border: 1px solid var(--ui-border); border-radius: 4px; background: var(--ui-surface); color: var(--ui-muted); font: 500 11px/1 var(--ui-mono); pointer-events: none;}.ui-navsearch-input:focus ~ .ui-navsearch-key, .ui-navsearch-open .ui-navsearch-key { display: none; }.ui-navsearch-clear { position: absolute; right: 5px; display: none; align-items: center; justify-content: center; width: 26px; height: 26px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--ui-muted); cursor: pointer;}.ui-navsearch-clear svg { width: 14px; height: 14px; }.ui-navsearch-clear:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-navsearch-clear:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-navsearch-open .ui-navsearch-clear { display: inline-flex; }@media (prefers-reduced-motion: reduce) { .ui-navsearch-input { transition: none; } }`component NavSearch(action: str = "/search", name: str = "q", value: str = "", placeholder: str = "Search", width: int = 160, wide: int = 280, shortcut: bool = true, label: str = "Search", id: str? = null, on_input = null, on_submit = null, class: str? = null) { style global nav_search_css state text = value state box = null let fid = id ?? `ui-navsearch-${name}` let put = (t: str) => { text = t on_input && on_input(t) } <form class={ui_cls(["ui-navsearch", text != "" && "ui-navsearch-open", class])} role="search" action={action} method="get" style={`--ui-navsearch-w: ${width}px; --ui-navsearch-wide: ${wide}px`} @mount={(el) => { box = el.querySelector("input") let onkey = (e) => { let t = e.target let typing = t.tagName == "INPUT" || t.tagName == "TEXTAREA" || t.tagName == "SELECT" || t.isContentEditable if e.key == "/" && !typing && !e.defaultPrevented && !e.ctrlKey && !e.metaKey && !e.altKey { e.preventDefault() box.focus() box.select() } } if shortcut { js("document.addEventListener", "keydown", onkey) } () => { if shortcut { js("document.removeEventListener", "keydown", onkey) } } }} @submit={(e) => { if on_submit != null { e.preventDefault() on_submit(text) } else { js("location.assign", `${action}?${url_encode(set({}, name, text))}`) } }}> <svg.ui-navsearch-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-navsearch-input type="search" id={fid} name={name} value={text} placeholder={placeholder} aria-label={label} aria-keyshortcuts={shortcut ? "/" : null} autocomplete="off" @input={(e) => { put(e.value) }} @keydown={(e) => { if e.key == "Escape" { e.preventDefault() if text != "" { put("") } else { e.target.blur() } } }}/> {shortcut && <kbd.ui-navsearch-key aria-hidden="true">/</kbd>} <button.ui-navsearch-clear type="button" aria-label="Clear search" tabindex={text != "" ? null : "-1"} @click={() => { put(""); box.focus() }}> <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> </form>}Related
AutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 downloadsCodeEditorA code textarea with line numbers, where Tab puts spaces in, Shift+Tab takes them out and Enter keeps the indent.5 views · 8 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 downloadsSearchInputA search box with a magnifier and a clear button; Escape clears, Enter submits its form.6 views · 5 downloadsFilterTabsTabs with counts that filter a list, one selected, moved between with the arrow keys; a hidden input holds the key.5 views · 6 downloadsSkipLink"Skip to content": the first link on a page, out of sight until the keyboard reaches it.5 views · 6 downloads
Thread
Thread 0 posts
On NavSearch: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.