Components

Dialog

Provide modal or non-modal surface with lifecycle, dismiss, focus, and layered interaction behavior.

Release Scope: Canonical Implementation: Core Site Visibility: Listed Adapters: React, Vanilla, Solid, Vue, Svelte Category: overlay

1. Live Preview

vanilla live preview activates on the client; source HTML keeps a readable preview island and primitive contract.

vanilla dialog preview activates on the client.

2. Installation

Install adapter-specific dependencies first, then complete setup via the installation guide. installation docs

pnpm add @nake-ui/primitives @nake-ui/adapter-vanilla @nake-ui/theme-vanilla

3. Usage

Minimal runnable usage for the current adapter.

import "@nake-ui/theme-vanilla/styles.css";
import { bindDialog } from "@nake-ui/adapter-vanilla";

const binding = bindDialog(nodes, items, options);
binding.destroy();

4. Examples

NestedDialog

Use nested dialog bindings so parent and nested layers keep independent open, focus, and dismiss lifecycles.

import "@nake-ui/theme-vanilla/styles.css";
import { bindDialog } from "@nake-ui/adapter-vanilla";

if (typeof window !== "undefined" && typeof document !== "undefined") {
  const parent = bindDialog(
    { trigger: parentTrigger, content: parentContent, overlay: parentOverlay, close: parentClose },
    {
      defaultOpen: false,
      modal: true,
      onOpenChange(nextOpen) {
        if (!nextOpen && child.controller.getOpen()) {
          child.controller.close();
          child.sync();
        }
      }
    }
  );

  const child = bindDialog(
    { trigger: childTrigger, content: childContent, overlay: childOverlay, close: childClose },
    {
      defaultOpen: false,
      modal: true,
      onOpenChange(nextOpen) {
        if (nextOpen && !parent.controller.getOpen()) {
          parent.controller.open();
          parent.sync();
        }
      }
    }
  );

  // cleanup on unmount
  parent.destroy();
  child.destroy();
}

parentTrigger.textContent = "Open parent dialog";
childTrigger.textContent = "Open nested dialog";

Custom Close Button

Compose a custom close button while preserving headless dialog close semantics.

import "@nake-ui/theme-vanilla/styles.css";
import { bindDialog } from "@nake-ui/adapter-vanilla";

if (typeof window !== "undefined" && typeof document !== "undefined") {
  const binding = bindDialog(
    { trigger, content, overlay, title, description, close },
    { defaultOpen: false, modal: true }
  );

  // cleanup on unmount
  binding.destroy();
}

close.textContent = "Dismiss";

No Close Button

Without a close button, dialog can still dismiss via overlay click, Escape, and outside dismiss.

import "@nake-ui/theme-vanilla/styles.css";
import { bindDialog } from "@nake-ui/adapter-vanilla";

if (typeof window !== "undefined" && typeof document !== "undefined") {
  const binding = bindDialog(
    { trigger, content, overlay, title, description },
    { defaultOpen: false, modal: true }
  );

  // cleanup on unmount
  binding.destroy();
}

trigger.textContent = "Open dialog";

Scrollable

Keep long dialog content usable with max-height and overflow scrolling on content.

import "@nake-ui/theme-vanilla/styles.css";
import { bindDialog } from "@nake-ui/adapter-vanilla";

if (typeof window !== "undefined" && typeof document !== "undefined") {
  const binding = bindDialog(
    { trigger, content, overlay, title, description, close },
    { defaultOpen: false, modal: true }
  );

  content.style.maxHeight = "12rem";
  content.style.overflow = "auto";

  // cleanup on unmount
  binding.destroy();
}

close.textContent = "Close";

5. RTL

The same structure running inside a `dir="rtl"` container.

vanilla dialog preview activates on the client.

6. Closure Coverage

Complete

Closure Score: 100% (19/19)

Required Checks

  • schemaRegistered
  • specDocumented
  • reactMapper
  • siteExampleCoverage
  • siteA11yCoverage
  • siteVisible
  • schemaValidatorCoverage
  • schemaAriaCoverage
  • specValidatorRulesSection
  • specTestChecklistSection
  • specSsrHydrationSection
  • reactSsrHydrationCoverage
  • reactRenderLevelSsrCoverage
  • solidSsrCoverage
  • coreImplemented
  • primitiveUnitTest
  • domContractSnapshot
  • vanillaBinding
  • adapterParitySnapshot

All required closure checks are currently satisfied.

7. Purpose / Non-goals

Provide modal or non-modal surface with lifecycle, dismiss, focus, and layered interaction behavior.

  • Visual overlay styling
  • Animation timeline management
  • Layout system ownership

8. Anatomy & Slot

  • trigger
  • overlay
  • content
  • title
  • description (optional)
  • close

9. DOM Contract

<button
  data-ui="dialog"
  data-slot="trigger"
  data-state="open"
  aria-haspopup="dialog"
  aria-expanded="true"
  aria-controls="dialog-content"
>
  Open dialog
</button>
<div data-slot="overlay" data-state="open"></div>
<section
  id="dialog-content"
  data-slot="content"
  data-state="open"
  data-expanded="true"
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-description"
>
  <h2 id="dialog-title" data-slot="title">Dialog title</h2>
  <p id="dialog-description" data-slot="description">Dialog description</p>
  <button data-slot="close" aria-controls="dialog-content">Close</button>
</section>

10. State & Events

State Model

  • open state reflected as data-state=&quot;open|closed&quot; on trigger/overlay/content
  • expanded state reflected as data-expanded=&quot;true|false&quot; on content
  • controlled mode: open + onOpenChange
  • uncontrolled mode: defaultOpen
  • modal flag: modal=true|false controls focus trap and aria-modal
  • nested dialogs keep independent open state and lifecycle ownership per layer

Event Model

  • trigger click toggles open/closed
  • close slot click closes
  • overlay click closes
  • outside pointerdown and outside focusin dismiss active dialog layer
  • Escape dismisses only the top-most active layer
  • nested behavior: opening child dialog must not dismiss parent; outside/Escape closes child first, then parent on next interaction

11. Keyboard & Focus

  • modal mode traps focus in active content
  • stack-aware layering pauses parent trap while child layer is active
  • closing a layer restores focus to the layer trigger
  • nested close flow is LIFO (child first, parent second)

12. ARIA

  • content: role=dialog
  • content: aria-modal (true in modal mode, false in non-modal mode)
  • content: aria-labelledby, aria-describedby
  • trigger: aria-haspopup=dialog, aria-expanded, aria-controls

13. Validator Rules

  • structure.missing-required-slot
  • aria.broken-controls-linkage
  • focus.trap-missing
  • focus.unrestored-on-close
  • keyboard.unhandled-escape
  • state.invalid-open-state
  • dom.missing-public-data-attr

14. Adapter Notes

Vanilla

  • Primary binder: bindDialog.
  • Binds explicit DOM nodes and should be cleaned up on unmount.

15. Example Mapping

16. API

Core

  • createDialogController value
  • DialogController type
  • DialogControllerOptions type

Vanilla

  • bindDialog function
  • DialogElements interface