Koala logo Design

Side trays

Side trays slide in from the right and load content via Alpine-AJAX. They are used for create/edit forms that don't warrant a full page navigation.

Demo

Click the button to open a side tray with a form. The tray has a header with title and close button, a scrollable content area with form fields, and a fixed footer with action buttons.

Trigger pattern

The trigger is an <a> link to the full-page route. Alpine.js overrides the click to open the tray and load content via x-target. Ctrl/Cmd+Click still navigates to the full page.

<a href="/full-page-route"
   x-on:click="if (!$event.ctrlKey && !$event.metaKey) {
       $dispatch('open-side-tray')
   }"
   x-target="side-tray-content">
    Open tray
</a>

Tray HTML structure

The AJAX response replaces the content of #side-tray-content. The tray shell lives in the layout. The response must include the header, scrollable body, and footer.

<div id="side-tray-content" class="flex-1 flex flex-col min-h-0">
    <!-- Header -->
    <div class="shrink-0 flex items-center justify-between px-6 sm:px-8 py-4">
        <h2 class="text-xl font-semibold text-gray-900 dark:text-white">
            Title
        </h2>
        <button type="button"
                x-on:click="$dispatch('close-side-tray')"
                class="text-gray-400 hover:text-gray-500
                       dark:text-gray-500 dark:hover:text-gray-400">
            <!-- close X SVG -->
        </button>
    </div>

    <!-- Scrollable content -->
    <div class="flex-1 overflow-y-auto px-6 sm:px-8 py-6">
        <form id="my-form" method="post"
              x-target="side-tray-content" novalidate autocomplete="off">
            <!-- form fields with koala-inline-validation-for -->
        </form>
    </div>

    <!-- Footer -->
    <div class="shrink-0 border-t border-gray-200 dark:border-gray-700
                px-6 sm:px-8 py-4 flex justify-end">
        <button type="submit" form="my-form"
                koala-loading koala-btn="Primary">
            Submit
        </button>
    </div>
</div>

Form inside tray

The form must use x-target="side-tray-content" so validation errors re-render inside the tray via AJAX. The submit button goes in the fixed footer, linked via the form attribute.

<form id="create-branch-form" method="post"
      x-target="side-tray-content" novalidate autocomplete="off">

    <div koala-inline-validation-for="Input.Name">
        <label asp-for="Input.Name"
               class="block mb-2.5 font-medium text-gray-900 dark:text-white">
            Branch name
        </label>
        <input asp-for="Input.Name" placeholder=""/>
        <span asp-validation-for="Input.Name" class="mt-2 block"></span>
    </div>
</form>

<!-- In the footer -->
<button type="submit" form="create-branch-form"
        koala-loading koala-btn="Primary">
    Create branch
</button>

Exception

If the submit button has name/value attributes (e.g. for action routing), it must be inside the form. Alpine-AJAX may not include external button name/value via the form attribute.

Redirect handling

On successful POST, do not return Redirect() — Alpine-AJAX follows the redirect, cannot find #side-tray-content in the destination page, and the tray goes blank. Instead, detect tray mode and return a Content() response with x-init to close the tray and navigate.

var isTray = Request.Headers.ContainsKey("X-Alpine-Target")
    && Request.Headers["X-Alpine-Target"]
        .ToString().Contains("side-tray-content");

if (isTray)
{
    return Content(
        $"""
        <div id="side-tray-content">
            <a href="{url}"
               x-target.push="main"
               x-init="$dispatch('close-side-tray');
                       $nextTick(() => $el.click())">
            </a>
        </div>
        """,
        "text/html");
}

return Redirect(url);

This returns a minimal HTML fragment with an auto-clicking link. Alpine-AJAX processes the link click via x-target.push="main", giving smooth navigation (only #main swaps) instead of a full page reload. The $dispatch('close-side-tray') closes the tray first.

Critical rules

  • Never use formnoajax on tray forms or their buttons — it breaks tray form submission
  • Never add x-on:ajax:after to forms with koala-inline-validation-for — field validation $ajax() events bubble to the form and trigger the handler prematurely
  • The form must have x-target="side-tray-content" to enable AJAX submission
  • Tray opening uses $dispatch('open-side-tray'); content loading uses x-target="side-tray-content" — they are independent
  • The tray shell content div must not have padding — padding belongs in the AJAX response
  • On validation failure, return Page() as normal — this re-renders the tray with errors
  • Side tray content areas use overflow-y-auto (not sm:overflow-visible)

Create branch