Bradley Kirton's Blog

Published on June 4, 2023

Go home

Creating a custom search control with HTMX and Hyperscript

I have decided to see what type of advanced widgets I can build without explicitly using Javascript. The first control I decided to build was a search widget for this site. The control is currently live and can be opened by pressing Ctrl+k. The search widget is built using the html5 dialog element, which is available in most browsers, HTMX and it's companion library Hyperscript.

Search widget

The control consists of the following HTML.

<dialog
    id="search"
    class="fixed top-0 left-1/4 m-0 py-5 rounded-t-none rounded-lg shadow-md w-[50%]">
    <div class="flex flex-col gap-3">
        <div>
            <input
                name="q"
                type="text"
                class="outline-none p-2 border-[1px] rounded-lg rounded-b-none h-10 w-full"
                autocomplete="off"
                autofocus="autofocus"
                placeholder="Begin typing to search this website..."
                hx-post="{% url 'bk-search' %}"
                hx-trigger="keyup delay:100ms"
                hx-target="#search-results"
                hx-swap="innerHTML"
                hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
                hx-include="[name='q']">
            <div id="search-results" class="prose-sm"></div>
        </div>
        <form>
            <button class="rounded bg-emerald-500 p-2 text-white font-bold w-full" formmethod="dialog">Close</button>
        </form>
    </div>
</dialog>

The #search-results is populated by HTMX with the following partial template.

{% if articles %}
    <div class="bg-white border-[1px] border-t-0 border-slate-300 rounded-b-lg z-50 max-h-[50vh] overflow-scroll">
    {% for article in articles %}
        <a href="{{ article.get_absolute_url }}" class="inline-block w-full p-2 bg-white hover:bg-slate-300 border-b-[1px] border-b-gray-300">
            <p class="m-0 font-bold text-lg">{{ article.title }}</p>
            <p class="m-0">{{ article.description }}</p>
        </a>
    {% endfor %}
    </div>
{% endif %}

The final piece of the puzzle is the event listener which calls showModal on the #search dialog. I came up with the following hyperscript which I have attached to the body tag.

<body
    _="on keydown[key is 'k' and ctrlKey is true]
       halt the event then
       set search to #search
       js(search) if (!search.open) { search.showModal() }
      ">
</body>