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
formnoajaxon tray forms or their buttons — it breaks tray form submission - Never add
x-on:ajax:afterto forms withkoala-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 usesx-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(notsm:overflow-visible)