- Purpose: Present blocking workflows (invite teammates, confirm actions) with server-rendered dialogs.
- Trigger: Button uses
hx-get="/modal/example"to render into an empty#modal-rootcontainer. - Endpoints:
GET /modal/examplereturns the dialog;GET /modal/closeclears the root;POST /modal/submithandles form submission and may emit HX-Trigger events or out-of-band toasts. - Structure: Wrapper
.greeble-modalwith backdrop and focusable panel.greeble-modal__panel; header includes a close button; body hosts form content. - Accessibility:
role="dialog",aria-modal="true", labelled viaaria-labelledby/aria-describedby; panel receives focus viatabindex="-1"; close button carriesaria-label. - Events: Emit
HX-Trigger: {"greeble:modal:open": true}when returning the partial andHX-Trigger-After-Swapto signal closing. - Theming hooks: Panel and actions inherit tokens;
.greeble-badge,.greeble-modal__actions,.greeble-checkboxexpose specific styling knobs. - Client behaviors: Load the bundled Hyperscript file to focus the modal panel and power delegated copy buttons.
GET /modal/examplereturns the modal partial. IncludeHX-Trigger: {"greeble:modal:open": true}if other components need to know the modal opened.GET /modal/closeshould return an empty string. HTMX clears#modal-rootimmediately.POST /modal/submithandles the form. On success, return an empty modal root plus an out-of-band toast to close the dialog and inform the user. On validation failure, return the modal partial with inline errors and set the status to 400 so HTMX keeps the dialog mounted.
{% include "greeble/modal.html" %}<button class="greeble-button greeble-button--primary" hx-get="/modal/example" hx-target="#modal-root" hx-swap="innerHTML">Invite teammates</button>
<div id="modal-root" aria-live="polite"></div>Return the modal partial example to hydrate the dialog. Close endpoints should respond with an empty string to remove the modal from the DOM. Include the shared Hyperscript bundle so the modal grabs focus when rendered:
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<script type="text/hyperscript" src="/static/greeble/hyperscript/greeble.hyperscript"></script>- Esc – Close the dialog (client should wire this to the close action when appropriate).
- Tab / Shift+Tab – Cycle focus within the dialog panel (focus trap).
- Enter – Activate focused button or submit the form when focus is inside a form control.
-
GET /modal/example
- 200 OK — returns modal partial
- Headers: optional
HX-Trigger: {"greeble:modal:open": true}
-
GET /modal/close
- 200 OK — returns empty string to clear
#modal-root
- 200 OK — returns empty string to clear
-
POST /modal/submit
- 200 OK (success) — returns:
- Out-of-band updates:
#modal-rootcleared (empty element) and toast container with success toast - Headers: optional
HX-Trigger-After-Swapto signal follow-up actions
- Out-of-band updates:
- 400 Bad Request (validation) — returns modal partial with inline errors; HTMX keeps dialog mounted
- 200 OK (success) — returns:
- Template: modal.html
- Partial: modal.partial.html
- Hyperscript: modal.hyperscript.html
- Styles: modal.css