• Combobox WIP

    A searchable dropdown built from a text input and the native Popover API. CSS handles all visual presentation; a small inline script filters the list and wires up selection.

    Each anchor-name must be unique per combobox on the page. Set it via an inline style and reference the same value as position-anchor on the popover.

    Default

  • <div class="combobox">
      <input
        type="search"
        placeholder="Search fruits..."
        style="anchor-name: --my-combobox"
        onfocus="
          if (
            !this.dataset.skip &&
            !this.nextElementSibling.matches(':popover-open')
          )
            this.nextElementSibling.showPopover();
          delete this.dataset.skip;
        "
        onblur="
          if (!this.nextElementSibling.contains(event.relatedTarget))
            setTimeout(
              () =>
                this.nextElementSibling.matches(':popover-open') &&
                this.nextElementSibling.hidePopover(),
            );
        "
        onkeydown="
          if (event.key === 'ArrowDown') {
            event.preventDefault();
            const f = this.nextElementSibling.querySelector(
              'li:not([hidden]) button',
            );
            f && f.focus();
          }
        "
        oninput="
          this.nextElementSibling
            .querySelectorAll('li')
            .forEach(
              (li) =>
                (li.hidden = !li.textContent
                  .toLowerCase()
                  .includes(this.value.toLowerCase())),
            )
        "
      />
      <div
        popover="manual"
        style="position-anchor: --my-combobox"
        onpointerdown="const b=event.target.closest('button');if(b){event.preventDefault();this.previousElementSibling.value=b.textContent.trim();this.hidePopover()}"
        onkeydown="
          const btns = [...this.querySelectorAll('li:not([hidden]) button')],
            i = btns.indexOf(event.target);
          if (event.key === 'ArrowDown') {
            event.preventDefault();
            (btns[i + 1] || btns[0])?.focus();
          } else if (event.key === 'ArrowUp') {
            event.preventDefault();
            i > 0 ? btns[i - 1].focus() : this.previousElementSibling.focus();
          } else if (event.key === 'Escape') {
            this.previousElementSibling.dataset.skip = 1;
            this.hidePopover();
            this.previousElementSibling.focus();
          } else if (event.key === 'Enter') {
            event.preventDefault();
            const b = event.target.closest('button');
            if (b) {
              this.previousElementSibling.value = b.textContent.trim();
              this.hidePopover();
              this.previousElementSibling.focus();
            }
          }
        "
        onfocusout="if(!this.contains(event.relatedTarget)&&event.relatedTarget!==this.previousElementSibling)setTimeout(()=>this.matches(':popover-open')&&this.hidePopover())"
      >
        <menu>
          <li><button class="ghost">Apple</button></li>
          <li><button class="ghost">Banana</button></li>
          <li><button class="ghost">Cherry</button></li>
          <li><button class="ghost">Mango</button></li>
          <li><button class="ghost">Pineapple</button></li>
        </menu>
      </div>
    </div>

    In a field

    Wrap with a <label> to attach a visible label.

    <label class="field">
      <span>Favourite fruit</span>
      <div class="combobox">
        <input
          type="search"
          placeholder="Search..."
          style="anchor-name: --my-combobox"
          onfocus="
            if (
              !this.dataset.skip &&
              !this.nextElementSibling.matches(':popover-open')
            )
              this.nextElementSibling.showPopover();
            delete this.dataset.skip;
          "
          onblur="
            if (!this.nextElementSibling.contains(event.relatedTarget))
              setTimeout(
                () =>
                  this.nextElementSibling.matches(':popover-open') &&
                  this.nextElementSibling.hidePopover(),
              );
          "
          onkeydown="
            if (event.key === 'ArrowDown') {
              event.preventDefault();
              const f = this.nextElementSibling.querySelector(
                'li:not([hidden]) button',
              );
              f && f.focus();
            }
          "
          oninput="
            this.nextElementSibling
              .querySelectorAll('li')
              .forEach(
                (li) =>
                  (li.hidden = !li.textContent
                    .toLowerCase()
                    .includes(this.value.toLowerCase())),
              )
          "
        />
        <div
          popover="manual"
          style="position-anchor: --my-combobox"
          onpointerdown="const b=event.target.closest('button');if(b){event.preventDefault();this.previousElementSibling.value=b.textContent.trim();this.hidePopover()}"
          onkeydown="
            const btns = [...this.querySelectorAll('li:not([hidden]) button')],
              i = btns.indexOf(event.target);
            if (event.key === 'ArrowDown') {
              event.preventDefault();
              (btns[i + 1] || btns[0])?.focus();
            } else if (event.key === 'ArrowUp') {
              event.preventDefault();
              i > 0 ? btns[i - 1].focus() : this.previousElementSibling.focus();
            } else if (event.key === 'Escape') {
              this.previousElementSibling.dataset.skip = 1;
              this.hidePopover();
              this.previousElementSibling.focus();
            } else if (event.key === 'Enter') {
              event.preventDefault();
              const b = event.target.closest('button');
              if (b) {
                this.previousElementSibling.value = b.textContent.trim();
                this.hidePopover();
                this.previousElementSibling.focus();
              }
            }
          "
          onfocusout="if(!this.contains(event.relatedTarget)&&event.relatedTarget!==this.previousElementSibling)setTimeout(()=>this.matches(':popover-open')&&this.hidePopover())"
        >
          <menu>
            <li><button class="ghost">Apple</button></li>
            <li><button class="ghost">Banana</button></li>
            <li><button class="ghost">Cherry</button></li>
          </menu>
        </div>
      </div>
    </label>

    Search 5021 icons

    Type a name to find icons from the Tabler icon set.