A modal and a side panel are the same idea in two shapes: an overlay that displays another BESPA page, controlled by a single state variable. When the variable is empty, the overlay is hidden; when it points at a path, that path’s handler is invoked and its output rendered inside the overlay.
Place a Modal widget at the top of your page, named by the state key it watches. Its child is the embedded page:
// mux is your *http.ServeMux (see Handlers & routing).
wf.Page().Add(
wf.AppBar("Dashboard"),
// The modal slot. Empty state.modal means hidden.
wf.Modal("modal").Add(
wf.EmbedHandler(mux.ServeHTTP, r, "GET",
wf.StateOf(r).Get("modal")+"?_back=^?modal=", nil),
),
// Anywhere on the page: open a path inside the modal.
wf.Link("?modal=/orders/new").Add("New order"),
wf.ButtonFilled("").
Add(wf.Icon("settings"), " Settings").
WithHref("?modal=/settings"),
)
Three things to notice:
wf.Modal("modal") — the argument names the state variable. Use a different name if you have several modals or want to namespace by page.EmbedHandler calls back into your mux to render whatever page the state value points at. The ?_back=^?modal= suffix tells the embedded page how to dismiss itself.A SidePanel is the same pattern with a different shape: a sliding pane attached to one edge of the viewport. Use it for inspectors, contextual help, or any secondary surface that should sit next to the main content rather than over it:
wf.Page().Add(
wf.SidePanel("panel").Add(
wf.EmbedHandler(mux.ServeHTTP, r, "GET",
wf.StateOf(r).Get("panel")+"?_back=^?panel=", nil),
),
wf.Link("?panel=/orders/123/details").Add("Show details"),
)
You can have both on the same page — each watches its own state key. To prevent one from showing while the other opens, write ?modal=/foo&panel= to open the modal while clearing the panel.
From inside the embedded page, two idioms close the modal: setting the state var to empty (via ^?modal= — the ^ means “act on the parent page”), or using a back button that reads the _back value the modal installed when it opened:
// Inside the embedded page — close the parent modal:
wf.Link("^?modal=").Add("Cancel"),
// Or a button helper that reads state._back:
wf.ButtonText("").Add("Back").WithHrefBack(),
The embedded page is a full BESPA page — its own AppBar, its own form, its own state. The framework keeps the parent and child state isolated; a state variable named name in the modal doesn’t collide with one of the same name in the parent.
The Modal widget renders with role="dialog" and aria-modal="true", so screen readers announce it as a modal context. The framework’s client runtime moves keyboard focus to the first focusable element inside the modal when it opens, and Esc dismisses the modal by clearing the controlling state variable (the same effect as a link to ^?modal=).
SidePanel does the same with role="complementary" — it’s a secondary surface, not a blocking dialog, so the page underneath remains interactive.
When a modal completes a task (a form posted, an item selected) and you want the result reflected on the parent page, write the result into a state variable on the parent at the same time you close the modal. The ^?key=value action-URL prefix does both in one move:
// In the embedded handler, after the user picked an item:
wf.Redirect(w, r, "^?modal=&picked="+id)
On the parent, any widget that called RedrawIfChanged(r, "picked") re-renders with the new value. The modal disappears (because its state variable went empty) and the parent surfaces the result — one round trip.
For modal forms that the user might dismiss by mistake, gate the close on whether anything was edited. Track “dirty” as a state variable the form’s inputs flip, and override the dismiss action:
if wf.StateOf(r).Get("dirty") == "1" {
closeHref = "?confirm=close" // open a confirm sub-modal
} else {
closeHref = "^?modal=" // close immediately
}
The framework doesn’t impose this — it’s an app-level pattern because what counts as “dirty” depends on your form.
A modal can open another modal — the embedded page can have its own Modal("sub") slot watching a different state key. It works, but it’s almost never the right design: two stacked overlays is usually a sign that one of the steps should be a full page instead. The convention in this codebase is to keep modal nesting to at most one level.
Basics → Embedded pages: overview — how modals compare to inline embeds and named frames.
Basics → Targeting frames — how the ^ and ~ path prefixes work and how to target a specific named frame.
Showcase → Overview — every demo can be opened in a modal or panel.