Koala logo Design

Filter popovers

When a list page has multiple filter dimensions (audience types, status, date range, specific people, etc.) and you want to keep the page header compact, use a single Filter button that opens a popover with the filter controls inside.

Live example

A Filter button that opens an Alpine popover with audience checkboxes, a specific-people search, and Reset / Apply actions. The active filter count appears in the trigger label when any audience flag is unticked or any specific entity is selected.

Show activity from

Specific people

Reference snippet

Copy-pasteable markup template for a filter popover wrapped around a GET form. Replace the audience checkboxes and the specific-people slot with whatever filter dimensions the page needs.

<form method="get" x-target.push="my-results">
    <div class="relative" x-data="{ open: false }">
        <button type="button"
                x-on:click="open = !open"
                koala-btn="Neutral"
                koala-btn-variant="Outlined"
                class="inline-flex items-center gap-2">
            <koala-icon name="Filter" size="Small" />
            <span>@filterButtonLabel</span>
            <koala-icon name="ChevronDown" size="Small" class="text-gray-400" />
        </button>

        <div x-show="open"
             x-on:click.outside="open = false"
             x-on:keydown.escape.window="open = false"
             x-transition:enter="transition ease-out duration-100"
             x-transition:enter-start="opacity-0 scale-95"
             x-transition:enter-end="opacity-100 scale-100"
             x-transition:leave="transition ease-in duration-75"
             x-transition:leave-start="opacity-100 scale-100"
             x-transition:leave-end="opacity-0 scale-95"
             x-cloak
             class="absolute z-20 mt-2 top-full right-0 w-[20rem] sm:w-[22rem] p-4
                    bg-white rounded-lg border border-gray-100
                    dark:bg-gray-700 dark:border-gray-600">

            <h3 class="font-semibold text-gray-900 dark:text-white mb-3">Show activity from</h3>

            <ul class="space-y-2">
                <li class="flex items-center">
                    <!-- Hidden false before each checkbox so an unchecked box still posts -->
                    <input type="hidden" name="IncludeClients" value="false" />
                    <input id="IncludeClients" name="IncludeClients" type="checkbox" value="true"
                           @(Model.IncludeClients ? "checked" : "")
                           class="w-4 h-4 bg-white border-gray-300 rounded ..." />
                    <label for="IncludeClients" class="ml-2 ...">Clients</label>
                </li>
                <!-- Repeat for Partners and Team -->
            </ul>

            <div class="border-t border-gray-200 dark:border-gray-600 my-3"></div>

            <h3 class="font-semibold text-gray-900 dark:text-white mb-3">Specific people</h3>
            <partial name="_PersonAutocomplete" />

            <div class="flex items-center justify-between gap-3 pt-2">
                <button type="button"
                        x-on:click="/* reset checkboxes + clear hidden inputs */"
                        koala-btn="Neutral" koala-btn-variant="Outlined">
                    Reset
                </button>
                <button type="submit"
                        x-on:click="open = false"
                        koala-btn="Primary">
                    Apply
                </button>
            </div>
        </div>
    </div>
</form>

Conventions

  • Trigger button is koala-btn="Neutral" koala-btn-variant="Outlined" with a leading <koala-icon name="..."> (typically Filter) and a trailing ChevronDown.
  • Active filter count appears in parentheses on the button label, e.g. Filter (2). The count includes any default-on audience flag that is unticked, plus any specific entities selected. Hidden when the count is zero.
  • Default state: all audience checkboxes ticked. Resetting returns to that state and clears any specific entities.
  • Popover uses Alpine x-data="{ open: false }" with x-on:click.outside="open = false" and x-on:keydown.escape.window. Anchored below-right of the trigger via absolute top-full right-0.
  • Apply submits the form. Reset clears in-popover state (uncheck back to default-on, remove any specific-entity hidden inputs) without submitting.
  • Mobile: same popover (no full-screen drawer fallback) unless positioning becomes awkward at narrow widths.
  • Wrap the popover in a <form method="get"> with x-target.push="results-container" so Apply re-renders the list via Alpine-AJAX.
  • Bind audience flags as separate booleans (e.g. IncludeClients, IncludePartners, IncludeTeam), not as a single [Flags] enum. Multi-value ?Audience=... query strings can collapse to the first value depending on the binder. Each bool defaults to true so a fresh page load shows everything; emit a hidden value="false" before each checkbox so unchecked boxes still post.
  • If selecting a specific entity in the popover redirects (e.g. via AddUserId) and the response replaces the results container the popover lives inside, append a popoverOpen=true query param to the redirect and seed the Alpine { open } from it. Apply, Reset and outside-click won't carry the flag, so they correctly close the popover on the next render.