Navigation

NavSearch

A search box for a navbar that grows when focused, with / to reach it and Escape to clear; a real form sends it.

#navigation#search#input#forms#keyboard#interactive

Demo

Preview
Acme

Press / anywhere to search; the box grows when focused.

A plain one: Enter sends q to its action.

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.qwe
main.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>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.