Skip to content
Sveltier
Mode
Primitives

Portal and modality

Build modal surfaces from parts: move content out of its container, make the page behind it inert, and stop the page from scrolling.

Together

A modal sheet is these primitives on one element, plus the focus trap and dismissable layer. Sveltier's Dialog and Sheet will be built the same way.

The sheet is portaled to the end of the page. Behind it the page is inert and does not scroll; Escape or a press outside closes it, and focus returns to the button.

TransferSheet.svelte
<script>
  import { Portal, dismissable, focusTrap, inertOthers, scrollLock } from 'sveltier';
  let open = $state(false);
  let opener = $state();
</script>

<button bind:this={opener} onclick={() => (open = true)}>Review transfer</button>

{#if open}
  <Portal>
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="sheet-title"
      {@attach focusTrap({ returnFocus: opener })}
      {@attach inertOthers()}
      {@attach scrollLock()}
      {@attach dismissable({ onDismiss: () => (open = false) })}
    >
      <h2 id="sheet-title">Confirm transfer</h2>
      …
    </div>
  </Portal>
{/if}

Portal

Portal renders its children somewhere else in the document, by default at the end of <body>. Theme, defaults, breakpoints and locale keep working inside, and content from a nested theme scope keeps that theme.

Toasts.svelte
<script>
  import { Portal } from 'sveltier';
</script>

<!-- Rendered at the end of <body>, or in the element target names. -->
<Portal target="#toasts">
  <Toast />
</Portal>
NameTypeDefaultDescription
targetHTMLElement | string'body'Where the content goes: an element, or a selector for one.
childrenSnippet–The content, rendered with the contexts and theme scope of where Portal is used.

Prefer the top layer when you can: an element shown with popover or <dialog> already sits above the page and stays in place in the DOM. Portaled content renders only in the browser, after hydration.

Inert page

inertOthers() makes everything outside the element inert: not focusable, not clickable, hidden from screen readers. Live regions stay active, so announcements still come through, and a modal inside another never reactivates what the outer one made inert. A native <dialog> opened with showModal() does this by itself.

Scroll lock

scrollLock() stops the page from scrolling while it is attached, and keeps the scrollbar's width as padding so nothing shifts. Locks are counted, so the page scrolls again only when the last modal closes. Modal dialogs need it too: they do not lock the page themselves.