Skip to content
CodeAtlas UI

Feedback

Dialog

A second-layer modal. The overlay blurs the page while the panel uses a solid surface.

<script>
  import { Dialog, Button } from "@codeatlasdev/ui";
  let open = $state(false);
</script>

<Dialog bind:open title="Confirm" description="This cannot be undone.">
  {#snippet actions()}
    <Button variant="ghost" onclick={() => (open = false)}>Cancel</Button>
    <Button variant="destructive" onclick={remove}>Delete</Button>
  {/snippet}
</Dialog>

The dialog renders a native modal <dialog>: focus is trapped, Escape closes and the panel carries aria-modal. For decisions that need an explicit answer use AlertDialog. On phones, prefer Sheet for bottom-anchored content.

Confirmation

The destructive confirm and cancel pattern.

Code
svelte
<script lang="ts">
  import { Dialog, Button } from '@codeatlasdev/ui';
  import { Trash2 } from '@lucide/svelte';

  let open = $state(false);
  let confirmed = $state(false);
</script>

<Button variant="destructive" onclick={() => ((confirmed = false), (open = true))}>
  {#snippet leading()}<Trash2 size={14} />{/snippet}
  Delete item
</Button>

<Dialog
  bind:open
  title="Delete this item?"
  description="This action cannot be undone. The item will be permanently removed from your workspace."
  size="sm"
>
  {#snippet actions()}
    <Button variant="ghost" onclick={() => (open = false)}>Cancel</Button>
    <Button variant="destructive" onclick={() => { confirmed = true; open = false; }}>Delete</Button>
  {/snippet}
</Dialog>

{#if confirmed}<p class="feedback" aria-live="polite">Item deleted.</p>{/if}

<style>
  .feedback { margin: 0; font-size: 13px; color: var(--ca-semantic-error); font-style: italic; }
</style>

Info

Informational content with a single action.

Code
svelte
<script lang="ts">
  import { Dialog, Button } from '@codeatlasdev/ui';
  import { Info } from '@lucide/svelte';

  let open = $state(false);
</script>

<Button variant="outlined" onclick={() => (open = true)}>
  {#snippet leading()}<Info size={14} />{/snippet}
  View details
</Button>

<Dialog bind:open title="About this component">
  <p class="body">
    <strong>Dialog</strong> renders a native modal <code>&lt;dialog&gt;</code>. It traps focus
    (Tab cycles inside), closes on Escape and exposes correct <code>aria-modal</code> semantics.
  </p>
  <p class="body">
    The overlay blurs the page while the panel uses a solid <code>--ca-surface-overlay</code>,
    so there is no double blur over the scrim.
  </p>
  {#snippet actions()}
    <Button variant="filled" onclick={() => (open = false)}>Got it</Button>
  {/snippet}
</Dialog>

<style>
  .body {
    font-size: var(--ca-font-size-body2);
    color: var(--ca-text-secondary);
    line-height: 1.6;
    margin: 0 0 10px;
  }
  .body:last-child { margin-bottom: 0; }
  .body strong { color: var(--ca-text-primary); }
  .body code {
    font-family: var(--ca-font-mono);
    font-size: 0.88em;
    color: var(--ca-brand-text);
    background: color-mix(in oklch, var(--ca-brand-main) 10%, transparent);
    padding: 1px 5px;
    border-radius: var(--ca-radius-xs);
  }
</style>

Form

A dialog holding a form, at the md size.

Code
svelte
<script lang="ts">
  import { Dialog, Button, TextField, Select, Stack } from '@codeatlasdev/ui';

  let open = $state(false);
  let name = $state('');
  let email = $state('');
  let role = $state('viewer');
  const roleOpts = [
    { value: 'admin', label: 'Admin' },
    { value: 'editor', label: 'Editor' },
    { value: 'viewer', label: 'Viewer' },
  ];
</script>

<Button variant="filled" onclick={() => (open = true)}>Invite member</Button>

<Dialog bind:open title="Invite team member" size="md">
  <Stack gap="3.5">
    <TextField label="Full name" bind:value={name} placeholder="Ada Lovelace" />
    <TextField label="Email" type="email" bind:value={email} placeholder="ada@company.com" />
    <Select label="Role" bind:value={role} options={roleOpts} />
  </Stack>
  {#snippet actions()}
    <Button variant="ghost" onclick={() => (open = false)}>Cancel</Button>
    <Button variant="filled" disabled={!name || !email} onclick={() => (open = false)}>Send invite</Button>
  {/snippet}
</Dialog>

Chromeless

No header, description or actions, and the body has no padding. The title becomes the visually hidden accessible name.

Code
svelte
<script lang="ts">
  import { Dialog, Button } from '@codeatlasdev/ui';

  let open = $state(false);
</script>

<Button variant="outlined" onclick={() => (open = true)}>Open chromeless</Button>

<Dialog bind:open title="Preview" size="md" chromeless>
  <div class="media">
    <p>Edge-to-edge content. Close with Esc or by clicking outside.</p>
    <Button variant="filled" onclick={() => (open = false)}>Close</Button>
  </div>
</Dialog>

<style>
  .media {
    display: grid;
    gap: 12px;
    padding: 24px;
    justify-items: start;
    color: var(--ca-text-secondary);
    font-size: var(--ca-font-size-body2);
  }
  .media p { margin: 0; }
</style>

Split

size="split" is 1100 by 720 with two columns from the md breakpoint and a single column below it. Combine it with chromeless.

Code
svelte
<script lang="ts">
  import { Dialog, Button, TextField, Stack } from '@codeatlasdev/ui';

  let open = $state(false);
  let name = $state('');
  let email = $state('');
</script>

<Button variant="filled" onclick={() => (open = true)}>Open split</Button>

<Dialog bind:open title="Lead details" size="split" chromeless>
  <div class="media">Media / summary</div>
  <div class="form">
    <Stack gap="3.5">
      <TextField label="Full name" bind:value={name} placeholder="Ada Lovelace" />
      <TextField label="Email" type="email" bind:value={email} placeholder="ada@company.com" />
      <Button variant="filled" onclick={() => (open = false)}>Save</Button>
    </Stack>
  </div>
</Dialog>

<style>
  .media {
    display: grid;
    place-items: center;
    min-height: 160px;
    background: color-mix(in oklab, var(--ca-brand-main) 12%, var(--ca-surface-overlay));
    color: var(--ca-text-secondary);
    font-size: var(--ca-font-size-body2);
  }
  .form { padding: 24px; overflow-y: auto; }
</style>

Props

PropTypeDefaultDescription
openbooleanfalseBindable open state.
titlestring–Header title. With chromeless it is the hidden accessible name.
descriptionstring–Subtitle below the title.
size'sm' | 'md' | 'lg' | 'xl' | 'split''md'Width: 400, 480, 600 or 820px. split is 1100 by 720 in two columns from md.
chromelessbooleanfalseOmits header, description and actions; the body has no padding. title and description stay available to screen readers.
childrenSnippet–Dialog body.
actionsSnippet–Footer buttons.
onOpenChange(open: boolean) => void–Called whenever the open state changes.
classstring''Extra classes on the panel.

Layers

Dialogs, sheets, drawers, menus, popovers and tooltips share one layer stack. Modals are native <dialog> elements opened with showModal(); menus, popovers and tooltips are popover elements. Both live in the browser’s top layer, so a layer opened later is always on top: there is no z-index to manage, and a menu inside a dialog just works. Anchored layers use CSS anchor positioning where the browser supports it and fall back to Floating UI elsewhere.

Escape and outside presses go to the topmost layer only, so a menu inside a dialog closes first and the dialog on the next press. Focus returns to the trigger when a layer closes.

A host that draws outside the DOM (a native view over a webview) can follow the stack with onLayerChange. It fires on every open and close with the open layers, bottom to top, and their rectangles:

import { onLayerChange } from "@codeatlasdev/ui";

const stop = onLayerChange(({ count, modal, layers }) => {
  nativeView.setParked(count > 0);
  // layers: [{ kind: "modal" | "popover" | "hint", rect: DOMRect }]
});

The same signal is a layerchange CustomEvent on document, with the detail as event.detail.