Koala logo Design

Switchers

Searchable dropdown modals for switching context (partner, branch, organisation). Full-screen on mobile, positioned below the trigger on desktop with items-start pt-[15vh].

Partner switcher

A modal with a search input and selectable list. The active item shows a checkmark. Each option displays an avatar and name. Used in the sidebar to switch between partners.

Switch partner

No partners found
<div x-data="{ open: false, search: '', selected: 'Koala Legal',
              names: ['koala legal', 'greenfield solicitors'] }"
     x-on:open-partner-switcher.window="search = ''; open = true;
         $nextTick(() => $refs.partnerSearch?.focus())"
     x-on:keydown.escape.window="if (open) { open = false; }">

    <!-- Backdrop -->
    <div x-show="open" x-cloak
         class="fixed inset-0 z-40 bg-gray-900/50
                dark:bg-gray-900/75"></div>

    <!-- Panel -->
    <div x-show="open" x-cloak
         x-on:click.self="open = false"
         class="fixed inset-0 z-50 flex items-center justify-center
                sm:items-start sm:pt-[15vh] sm:p-4">
        <div class="bg-white dark:bg-gray-800
                    sm:border border-gray-200 dark:border-gray-700
                    rounded-none sm:rounded-lg
                    w-full h-full sm:h-auto sm:max-w-md
                    overflow-y-auto">

            <!-- Header -->
            <div class="flex items-center justify-between p-4
                        border-b border-gray-200 dark:border-gray-700">
                <h3 class="text-lg font-semibold text-gray-900
                           dark:text-white">Switch partner</h3>
                <button type="button" x-on:click="open = false"
                        class="text-gray-400 hover:text-gray-500
                               dark:hover:text-gray-300">
                    <koala-icon name="X" />
                </button>
            </div>

            <!-- Search -->
            <div class="p-4 border-b border-gray-200
                        dark:border-gray-700">
                <div class="relative">
                    <div class="absolute inset-y-0 start-0
                                flex items-center ps-3
                                pointer-events-none">
                        <koala-icon name="Search" size="Small"
                                   class="text-gray-400" />
                    </div>
                    <input type="text"
                           x-ref="partnerSearch"
                           x-model="search"
                           placeholder="Search partners..."
                           class="bg-white dark:bg-gray-700
                                  border border-gray-200
                                  dark:border-gray-600
                                  text-gray-900 dark:text-white
                                  rounded-lg block w-full ps-9
                                  px-3 py-2.5" />
                </div>
            </div>

            <!-- Options -->
            <div class="max-h-72 space-y-1 overflow-y-auto p-2">
                @foreach (var partner in partners)
                {
                    var isCurrent = partner.Id == currentBranch.PartnerId;
                    <form method="post" action="/partner/switch"
                          x-target.push="main"
                          data-name="@partner.Name.ToLowerInvariant()"
                          x-show="!search || $el.dataset.name
                              .includes(search.toLowerCase())">
                        <input type="hidden" name="partnerId"
                               value="@partner.Id" />
                        <button type="submit"
                                class="flex w-full items-center gap-3
                                       px-3 py-2.5 text-left rounded-lg
                                       @(isCurrent
                                           ? &quot;bg-gray-100 ...&quot;
                                           : &quot;text-gray-700 ...&quot;)">
                            <koala-partner-avatar size="Medium"
                                partner-id="@partner.Id"
                                name="@partner.Name"
                                has-avatar="@(...)" />
                            <span class="truncate flex-1">
                                @partner.Name
                            </span>
                            @if (isCurrent)
                            {
                                <!-- Checkmark SVG -->
                            }
                        </button>
                    </form>
                }

                <!-- Empty state -->
                <div x-show="search && !names.some(
                         n => n.includes(search.toLowerCase()))"
                     x-cloak
                     class="px-4 py-6 text-center text-gray-500
                            dark:text-gray-400">
                    No partners found
                </div>
            </div>
        </div>
    </div>
</div>

Sidebar trigger

The switcher is opened from a trigger in the sidebar header. The trigger shows the current selection with an avatar and a chevron icon. It dispatches a custom window event to open the modal.

<a href="/partner/switch"
   x-data
   x-on:click="if (!$event.ctrlKey && !$event.metaKey) {
       $event.preventDefault();
       $dispatch('open-partner-switcher');
   }"
   class="flex w-full items-center gap-3 rounded-lg
          px-2 py-1.5 hover:bg-white/8">
    <koala-partner-avatar size="Small"
        partner-id="@currentBranch.PartnerId"
        name="@currentBranch.PartnerName"
        has-avatar="@(...)" />
    <div class="min-w-0 flex-1 text-left">
        <div class="truncate text-sm text-secondary-300">
            @currentBranch.PartnerName
        </div>
    </div>
    <svg class="h-3.5 w-3.5 shrink-0 text-secondary-300">
        <path d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path>
    </svg>
</a>

Key rules

  • Full-screen on mobile, positioned with sm:items-start sm:pt-[15vh] on desktop
  • Search input auto-focuses when the modal opens via $nextTick(() => $refs.search?.focus())
  • Options are filtered client-side using x-show with includes()
  • The active option shows a checkmark icon (text-fg-brand)
  • Each option is a <form> that POSTs to a switch endpoint
  • Escape key closes the modal via x-on:keydown.escape.window
  • Empty state shown when search yields no results
  • Trigger uses $dispatch() to open the modal, with Ctrl/Cmd+Click fallback to full page