Navigation

Dialog

<oblyx-dialog> is a centered modal built on the native <dialog> element and showModal(). Pair it with <oblyx-dialog-header>,<oblyx-dialog-body> and <oblyx-dialog-footer> for a title, a scrolling body and an action row. Set the open attribute, or the open property from a click handler, to show it.

Basic dialog

This can't be undone. All boards, cards and history are removed immediately. Delete
HTMLKotlin
<button type="button" onclick="document.getElementById('ox-doc-dialog-basic').open = true">
  Delete project
</button>
<oblyx-dialog id="ox-doc-dialog-basic" label="Delete project?">
  <oblyx-dialog-header heading="Delete project?"></oblyx-dialog-header>
  <oblyx-dialog-body>This can't be undone. All boards, cards and history are removed immediately.</oblyx-dialog-body>
  <oblyx-dialog-footer>
    <oblyx-button variant="destructive">Delete</oblyx-button>
  </oblyx-dialog-footer>
</oblyx-dialog>
button {
  onClick = "document.getElementById('ox-doc-dialog-basic').open = true"
  +"Delete project"
}
oblyxDialog(label = "Delete project?") {
  id = "ox-doc-dialog-basic"
  oblyxDialogHeader(heading = "Delete project?")
  oblyxDialogBody { +"This can't be undone. All boards, cards and history are removed immediately." }
  oblyxDialogFooter {
    oblyxButton(variant = OblyxButtonVariant.DESTRUCTIVE) { +"Delete" }
  }
}

Composition

oblyx-dialog-header, oblyx-dialog-body and oblyx-dialog-footer are plain child elements, not slots, arranged by CSS in DOM order: the same composition model as card. Any subset, any order. The header is the one region with a real job beyond layout: its heading becomes the dialog's visible title and its accessible name (via aria-labelledby), and it renders a close button by default. A dialog with no header must set label directly on <oblyx-dialog> instead: one of the two is required, and a missing one is logged to the console rather than silently shipping an unlabeled dialog.

Opening and dismissing

Setting dismissible to false (default true) turns off every ambient way to close the dialog at once: Escape, a backdrop click, and the header's own close button all become no-ops. There's no separate flag for each: a dialog that shouldn't lose its input to a stray Escape shouldn't lose it to a stray backdrop click either. The only way out is a footer action that sets open = false itself, or a cancel event listener that calls event.preventDefault() conditionally (to build something like a "discard changes?" confirmation) and lets it through otherwise.

dismissible and the header's own closable both default to true, so bare presence (or omitting the attribute) means true, matching a native disabled="". Writing the literal string dismissible="false" in server-rendered markup turns it off, same as the example below: no client-side script required.

Escape is intercepted at keydown, not through the native <dialog>cancel event: in Chromium that event reports cancelable: false, so preventDefault() on it does nothing and the platform closes the dialog regardless. The cancel listener stays wired as a fallback, but the real interception point (and the one this library's own dismissible gate depends on) is the keydown handler. A backdrop click is detected by comparing event.target to event.currentTarget: a real ::backdrop click always targets the <dialog> itself, since the backdrop isn't a child node, while a click on any actual content inside targets that content instead.

Not dismissible

No Escape, no backdrop click, no close button. Only the footer's own Confirm button closes it.

Enter the code we sent before continuing.
HTMLKotlin
<button type="button" onclick="document.getElementById('ox-doc-dialog-required').open = true">
  Start required step
</button>
<oblyx-dialog id="ox-doc-dialog-required" dismissible="false" label="Confirm your email">
  <oblyx-dialog-header heading="Confirm your email" closable="false"></oblyx-dialog-header>
  <oblyx-dialog-body>Enter the code we sent before continuing.</oblyx-dialog-body>
  <oblyx-dialog-footer>
    <button type="button" onclick="document.getElementById('ox-doc-dialog-required').open = false">
      Confirm
    </button>
  </oblyx-dialog-footer>
</oblyx-dialog>
button {
  onClick = "document.getElementById('ox-doc-dialog-required').open = true"
  +"Start required step"
}
oblyxDialog(dismissible = false, label = "Confirm your email") {
  id = "ox-doc-dialog-required"
  oblyxDialogHeader(heading = "Confirm your email", closable = false)
  oblyxDialogBody { +"Enter the code we sent before continuing." }
  oblyxDialogFooter {
    // A plain <button> here, not oblyxButton: this row needs an inline
    // onclick to close the dialog programmatically, and oblyx-button has
    // no click-handler attribute of its own to bind one through.
    button {
      onClick = "document.getElementById('ox-doc-dialog-required').open = false"
      +"Confirm"
    }
  }
}

Focus

Focus trapping and Tab-cycling inside the dialog come from the platform: showModal() makes the rest of the document inert for as long as the dialog is open, so Tab can never leave it. Nothing in Oblyx re-implements that.

What Oblyx does own is where focus lands and where it goes back to. On open, focus moves to [data-autofocus] if one exists inside the dialog, otherwise to the header's title (so a screen reader announces it immediately), otherwise to the browser's own default (the first focusable descendant, or the dialog itself). On close, focus always returns to whatever element triggered the open, and only after dialog.close() has actually run, since the rest of the page is still inert until that call returns.

Autofocus a specific field

data-autofocus wins over the header title.

Save
HTMLKotlin
<button type="button" onclick="document.getElementById('ox-doc-dialog-autofocus').open = true">
  Rename board
</button>
<oblyx-dialog id="ox-doc-dialog-autofocus" label="Rename board">
  <oblyx-dialog-header heading="Rename board"></oblyx-dialog-header>
  <oblyx-dialog-body>
    <label for="ox-doc-dialog-autofocus-input">Board name</label>
    <input id="ox-doc-dialog-autofocus-input" data-autofocus type="text" value="Q3 roadmap" />
  </oblyx-dialog-body>
  <oblyx-dialog-footer>
    <oblyx-button variant="primary">Save</oblyx-button>
  </oblyx-dialog-footer>
</oblyx-dialog>
button {
  onClick = "document.getElementById('ox-doc-dialog-autofocus').open = true"
  +"Rename board"
}
oblyxDialog(label = "Rename board") {
  id = "ox-doc-dialog-autofocus"
  oblyxDialogHeader(heading = "Rename board")
  oblyxDialogBody {
    label {
      htmlFor = "ox-doc-dialog-autofocus-input"
      +"Board name"
    }
    input(type = InputType.text) {
      id = "ox-doc-dialog-autofocus-input"
      attributes["data-autofocus"] = ""
      value = "Q3 roadmap"
    }
  }
  oblyxDialogFooter {
    oblyxButton(variant = OblyxButtonVariant.PRIMARY) { +"Save" }
  }
}

Loading dialog content with HTMX

A common shape in a server-rendered app: the trigger both opens the dialog and issues an hx-get that swaps real content into <oblyx-dialog-body>, instead of the page shipping every dialog's full content up front. Opening the dialog and starting the fetch are two independent actions on the same click: open = true shows the dialog immediately with a loading state, and HTMX swaps the fetched fragment in whenever the response arrives, wherever that lands relative to the animation.

<oblyx-dialog-body> is exactly as independently constructible as every other Oblyx child element (see BRAINSTORM.md's "fragments render partial subtrees" rule), so the swapped-in response can be a bare fragment with no <oblyx-dialog> wrapper at all: HTMX upgrades it in place the moment it lands.

The example below performs a real hx-get, a real fetch and a real DOM swap: open your browser's network panel and watch it happen. The response is a fixed file on this site rather than something a server rendered for that specific request, since this docs site has no backend of its own. The fragment contains a real <oblyx-checkbox>, unstyled and inert until the swap lands, so you can watch it upgrade into a working control the moment HTMX inserts it.

The HTML tab's hx-get path points at that static file so the live preview below actually works; the Kotlin tab uses /fragments/delete-project-body instead, a route path a real Ktor server would serve dynamically rather than a static asset path.

Dialog body loaded on demand

Watch the network panel: the checkbox below arrives as plain markup and upgrades on arrival.

Loading… Delete
HTMLKotlin
<button
  type="button"
  hx-get="/examples/overlays/delete-project-body.html"
  hx-target="#ox-doc-dialog-htmx-body"
  hx-swap="innerHTML"
  onclick="document.getElementById('ox-doc-dialog-htmx').open = true"
>
  Delete project (loads content)
</button>
<oblyx-dialog id="ox-doc-dialog-htmx" label="Delete project?">
  <oblyx-dialog-header heading="Delete project?"></oblyx-dialog-header>
  <oblyx-dialog-body id="ox-doc-dialog-htmx-body">Loading…</oblyx-dialog-body>
  <oblyx-dialog-footer>
    <oblyx-button variant="destructive">Delete</oblyx-button>
  </oblyx-dialog-footer>
</oblyx-dialog>
button {
  attributes["hx-get"] = "/fragments/delete-project-body"
  attributes["hx-target"] = "#ox-doc-dialog-htmx-body"
  attributes["hx-swap"] = "innerHTML"
  onClick = "document.getElementById('ox-doc-dialog-htmx').open = true"
  +"Delete project (loads content)"
}
oblyxDialog(label = "Delete project?") {
  id = "ox-doc-dialog-htmx"
  oblyxDialogHeader(heading = "Delete project?")
  oblyxDialogBody { id = "ox-doc-dialog-htmx-body"; +"Loading…" }
  oblyxDialogFooter {
    oblyxButton(variant = OblyxButtonVariant.DESTRUCTIVE) { +"Delete" }
  }
}

Reference

<oblyx-dialog>

AttributeTypeDefaultDescription
openbooleanfalseWhether the dialog is open. Setting this directly always takes effect: it is the "force open/closed" path, matching a native <dialog>'s own close(). For a dismissal a user can cancel, see dismiss.
dismissiblebooleantrueWhether Escape and a backdrop click attempt to close the dialog at all.
labelstring | undefinedAccessible name, used when no <oblyx-dialog-header heading> (or <oblyx-drawer>'s own light content) provides one. Required in that case; its absence is flagged loudly, not silently ignored.

Events

EventTypeDescription
closeEvent

<oblyx-dialog-header>

AttributeTypeDefaultDescription
headingstring | undefinedThe dialog's visible title, and also its accessible name source. Optional only because a header might exist purely to hold a close button/actions row with the title supplied elsewhere via the dialog's own label; a header with neither is a defect and is flagged loudly.
subtitlestring | undefinedSecondary line under the title.
closablebooleantrueWhether to render the built-in close (×) button.
close-labelstringCloseAccessible name for the close button.

<oblyx-dialog-body>

This element declares no attributes.

<oblyx-dialog-footer>

This element declares no attributes.