Modal

Native <dialog> styling. The framework supplies the look and ships two open mechanisms: showModal() (the full modal contract, requires one line of JS) and :target (zero JS, with documented trade-offs).

Live

Interactive: JS-free (:target)

Open (no JS)

<p>
<a class="button" href="#demo-modal-jsfree-dialog">Open (no JS)</a>
</p>
<dialog class="modal" id="demo-modal-jsfree-dialog">
<a class="modal__dismiss" href="#" aria-label="Close"></a>
<div class="modal__content">
  <header class="modal__header">
    <h3 style="margin: 0;">Read-only notice</h3>
    <a href="#" aria-label="Close" class="button" style="font-size: 1.25rem; line-height: 1; padding: 0.25rem 0.5rem; min-width: 0;">&times;</a>
  </header>
  <div class="modal__body">
    <p>
      This dialog opens via <code>:target</code>. Clicking the
      tinted area outside the panel navigates the URL hash away,
      which closes the modal. <strong>Caveat:</strong> Escape does
      nothing; focus is not trapped; the page behind is not
      <code>inert</code>.
    </p>
  </div>
  <footer class="modal__footer">
    <a class="button" href="#">Close</a>
  </footer>
</div>
</dialog>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-modal')
);
Playground

Interactive: JS-enhanced (showModal())

<p>
<button class="button" type="button" onclick="document.getElementById('demo-modal-js-dialog').showModal()">Open (with JS)</button>
</p>
<dialog class="modal" id="demo-modal-js-dialog">
<form method="dialog" class="modal__content">
  <header class="modal__header">
    <h3 style="margin: 0;">Confirm action</h3>
    <button type="button" aria-label="Close" formnovalidate
      onclick="this.closest('dialog').close()"
      style="font-size: 1.25rem; line-height: 1; padding: 0.25rem 0.5rem; min-width: 0;">&times;</button>
  </header>
  <div class="modal__body">
    <p>
      This dialog opens via <code>dialog.showModal()</code>. Escape
      closes it, focus is trapped inside, the page behind is marked
      <code>inert</code>. Clicking outside the dialog also closes it
      (wired below). Closing returns the button's
      <code>value</code> via <code>dialog.returnValue</code>.
    </p>
  </div>
  <footer class="modal__footer">
    <button value="cancel">Cancel</button>
    <button value="ok" class="button is-primary">Overwrite</button>
  </footer>
</form>
</dialog>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-modal')
);
Playground

Open mechanisms

Capabilitydialog.showModal():target fragment
JS requiredOne call: dialog.showModal()None
Visual backdropYes (UA paints ::backdrop)Yes (framework emulates via ::before)
Background scroll lockYes (UA inerts the page)Yes (framework: html:has(.modal:target) { overflow: hidden; })
Focus trapped inside dialogYes (UA-managed)No (CSS cannot move focus)
Background marked inert (clicks/tabs blocked)YesNo (CSS cannot suppress events on siblings)
Escape key closesYesNo (CSS has no key listener)
Click-outside closesOptional (consumer wires)Optional: include <a href="#" class="modal__dismiss">
Close from inside<form method="dialog"> submit, dialog.close()Any link to a different fragment, e.g. <a href="#">
Suitable for a11y-critical confirmationsYesNo (use showModal())

Pick deliberately. The :target path is JS-free but is not a true modal: focus is not trapped, the background is not inert, and Escape does not close. Screen-reader and keyboard users can interact with the page behind it. Use :target for read-only notices, link cards, or non-blocking surfaces; reserve showModal() for decisions that must own the user's attention until resolved.

Markup: showModal() + method="dialog"

dialog markup
<dialog class="modal" id="confirm">
  <form method="dialog" class="modal__content">
    <header class="modal__header">
      <h2>Confirm action</h2>
      <button type="button" aria-label="Close" formnovalidate
        onclick="this.closest('dialog').close()">&times;</button>
    </header>
    <div class="modal__body">
      <p>This will overwrite your saved changes.</p>
    </div>
    <footer class="modal__footer">
      <button value="cancel">Cancel</button>
      <button value="ok" class="button is-primary">Overwrite</button>
    </footer>
  </form>
</dialog>

<button onclick="document.getElementById('confirm').showModal()">
  Open modal
</button>

<script>
  // Close on click outside the dialog content. `<dialog>` dispatches the
  // click on the dialog itself when the user clicks the backdrop area; the
  // bounding-rect check avoids closing on clicks inside the dialog padding.
  document.getElementById('confirm').addEventListener('click', (event) => {
    const dialog = event.currentTarget;
    if (event.target !== dialog) return;
    const rect = dialog.getBoundingClientRect();
    const inside = event.clientY >= rect.top && event.clientY <= rect.bottom
      && event.clientX >= rect.left && event.clientX <= rect.right;
    if (!inside) dialog.close();
  });
</script>

Markup: :target fallback

The optional <a class="modal__dismiss"> first child is the click-outside-to-close target. Omit it if you want the user to close only via the explicit "Close" link.

:target fallback
<a href="#info">Open</a>

<dialog class="modal" id="info">
  <!-- Optional: click-outside-to-close. -->
  <a class="modal__dismiss" href="#" aria-label="Close"></a>

  <div class="modal__content">
    <header class="modal__header">
      <h2>Read-only notice</h2>
      <a href="#" aria-label="Close" class="button">&times;</a>
    </header>
    <div class="modal__body">
      <p>This dialog opens via the URL fragment. Close by clicking outside, by following another link, or by activating "Close" below.</p>
    </div>
    <footer class="modal__footer">
      <a href="#" class="button">Close</a>
    </footer>
  </div>
</dialog>

Scope

  • Confirmations, destructive-action warnings, focused single-task forms.
  • Inline page notices that should not trap focus: Alert (rendered in flow, not a <dialog>).

Accessibility

  • A visible close button is required inside .modal__header or .modal__footer for every :target-driven dialog. The .modal__dismiss click-outside anchor is click-only — there is no keyboard equivalent for the backdrop area, so keyboard users dismiss the modal exclusively through the explicit Close button.
  • The :target mechanism cannot trap focus, mark the page behind inert, or listen for Escape. For full keyboard accessibility (focus trap, ESC-to-close, background inertness) use dialog.showModal() — the browser-native path emitted by the 'js' control mode.
  • Label the close control with aria-label="Close" so screen readers announce its purpose when the icon (&times;) carries no text.

Variables

VariableDefaultNotes
$selector'.modal'Applied to <dialog>.
$content-selector'.modal__content'Inner flex container.
$header-selector'.modal__header'Title row.
$body-selector'.modal__body'Prose body.
$footer-selector'.modal__footer'Action row, right-aligned.
$dismiss-selector'.modal__dismiss'Optional click-outside anchor inside :target-opened dialogs.
$max-width32remHard cap on dialog width.
$padding$size-normalSurface padding.
$gap$size-small (0.75rem)Vertical gap between header / body / footer slots inside .modal__content.
$border0Resets the Chromium <dialog> UA border so theming owns the surface edge.
$border-radius$global-border-radiusSurface radius.
$backdrop-colorrgba(0,0,0,0.55)Fallback tint when no theme provides --kc-modal-backdrop. Applied via ::backdrop (showModal) and ::before (:target).
$backdrop-blur6pxBlur applied to content behind the backdrop. Set to 0 to disable.

See Combinations and light variants for $enable-light, $default-combination, and $controls — cross-cutting plumbing common to every theme-aware component.

Override example

app.scss
@use '@adnap/krysalicss/module/modal' with (
  $max-width: 40rem,
  $padding: 1.5rem,
  $backdrop-color: rgba(0, 0, 0, 0.7),
);

Tokens consumed

TokenUsed for
--kc-modal-bg / --kc-modal-fgDefault surface.
--kc-modal-backdropBackdrop tint, swapped per theme. Default theme leaves it unset (falls back to $backdrop-color); the dark theme emits a translucent-light wash so the dark surface stands out.