Overlay

Modal

A dialog modal with header, body, and footer sections. Supports scrollable content, fullscreen mode, custom headers, and non-dismissible state.

Basic Usage

Use bind:open to control visibility.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';

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

<Button label="Open Modal" onclick={() => (open = true)} />

<Modal
  bind:open
  title="Basic Modal"
  description="This is a simple modal with a title and description."
>
  {#snippet body()}
    <p>This is the body content of the modal.</p>
  {/snippet}
</Modal>

With Footer

Add action buttons in the footer.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';

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

<Button label="Open Modal" onclick={() => (open = true)} />

<Modal
  bind:open
  title="Confirm Action"
  description="Are you sure you want to proceed?"
>
  {#snippet body()}
    <p>This action cannot be undone. Please confirm to continue.</p>
  {/snippet}

  {#snippet footer()}
    <Button variant="outline" color="surface" label="Cancel" onclick={() => (open = false)} />
    <Button label="Confirm" onclick={() => (open = false)} />
  {/snippet}
</Modal>

Scrollable

Enable scrolling for long content.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';

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

<Button label="Open Scrollable" onclick={() => (open = true)} />

<Modal
  bind:open
  title="Terms of Service"
  scrollable
>
  {#snippet body()}
    <div class="space-y-4">
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit...</p>
      <!-- Long scrollable content -->
    </div>
  {/snippet}
</Modal>

Fullscreen

Fill the entire viewport.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';

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

<Button label="Open Fullscreen" onclick={() => (open = true)} />

<Modal
  bind:open
  title="Fullscreen Modal"
  fullscreen
>
  {#snippet body()}
    <p>This modal fills the entire viewport.</p>
  {/snippet}
</Modal>

Custom Header

Replace the default header with a custom layout.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';

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

<Button label="Open Custom Header" onclick={() => (open = true)} />

<Modal bind:open>
  {#snippet header()}
    <div class="flex items-center gap-3">
      <div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
        <span class="text-primary text-lg">!</span>
      </div>
      <div>
        <h3 class="text-lg font-semibold">Custom Header</h3>
        <p class="text-sm text-on-surface/60">With a custom icon layout</p>
      </div>
    </div>
  {/snippet}

  {#snippet body()}
    <p>The header snippet lets you fully customize the header area.</p>
  {/snippet}
</Modal>

Nested Modals

Stack modals on top of each other.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';

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

<Button label="Open Outer Modal" onclick={() => (outerOpen = true)} />

<Modal bind:open={outerOpen} title="Outer Modal">
  {#snippet body()}
    <p>This is the outer modal. Click below to open a nested modal.</p>
    <Button label="Open Inner Modal" variant="outline" onclick={() => (innerOpen = true)} class="mt-4" />
  {/snippet}

  {#snippet footer()}
    <Button variant="outline" label="Close" onclick={() => (outerOpen = false)} />
  {/snippet}
</Modal>

<Modal bind:open={innerOpen} title="Inner Modal" description="This is a nested modal on top of the outer one.">
  {#snippet body()}
    <p>You can stack modals on top of each other.</p>
  {/snippet}

  {#snippet footer()}
    <Button label="Close Inner" onclick={() => (innerOpen = false)} />
  {/snippet}
</Modal>

With Form & SelectMenu

A practical example combining Modal with form controls including a searchable SelectMenu.

<script lang="ts">
  import { Modal, Button, Input, Textarea, Select, SelectMenu, FormField } from 'sv5ui';

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

<Button label="Create Task" onclick={() => (open = true)} />

<Modal bind:open title="Create New Task" description="Fill in the details below.">
  {#snippet body()}
    <form class="space-y-4">
      <FormField label="Title" required>
        <Input placeholder="Task title..." />
      </FormField>

      <FormField label="Assignee">
        <SelectMenu
          placeholder="Search team members..."
          items={[
            { value: 'alice', label: 'Alice Martin', avatar: { src: 'https://i.pravatar.cc/40?u=alice', alt: 'Alice' } },
            { value: 'bob', label: 'Bob Wilson', avatar: { src: 'https://i.pravatar.cc/40?u=bob', alt: 'Bob' } },
            { value: 'carol', label: 'Carol Lee', avatar: { src: 'https://i.pravatar.cc/40?u=carol', alt: 'Carol' } },
            { value: 'dave', label: 'Dave Kim', avatar: { src: 'https://i.pravatar.cc/40?u=dave', alt: 'Dave' } }
          ]}
        />
      </FormField>

      <FormField label="Priority">
        <Select
          placeholder="Select priority"
          items={[
            { value: 'low', label: 'Low' },
            { value: 'medium', label: 'Medium' },
            { value: 'high', label: 'High' },
            { value: 'urgent', label: 'Urgent' }
          ]}
        />
      </FormField>

      <FormField label="Description" hint="Optional">
        <Textarea placeholder="Describe the task..." autoresize />
      </FormField>
    </form>
  {/snippet}

  {#snippet footer()}
    <Button variant="outline" color="surface" label="Cancel" onclick={() => (open = false)} />
    <Button label="Create Task" onclick={() => (open = false)} />
  {/snippet}
</Modal>

Non-Dismissible

Prevent closing via overlay click or Escape.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';

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

<Button label="Open Non-dismissible" onclick={() => (open = true)} />

<Modal
  bind:open
  title="Important Notice"
  dismissible={false}
>
  {#snippet body()}
    <p>This modal cannot be closed by clicking outside or pressing Escape. You must use the button below.</p>
  {/snippet}

  {#snippet footer()}
    <Button label="I Understand" onclick={() => (open = false)} />
  {/snippet}
</Modal>

UI Slots

Use the ui prop to override classes on internal elements.

SlotDescription
overlayBackdrop overlay
contentModal content container
headerHeader section
wrapperTitle/description wrapper
titleTitle text
descriptionDescription text
actionsActions area (between title and close)
bodyMain content area
footerFooter section
closeClose button area

Snippets

SnippetDescription
childrenTrigger element (clicking opens modal)
contentReplace entire default layout
headerCustom header (replaces default)
titleSlotCustom title (overrides title prop)
descriptionSlotCustom description
actionsActions between title and close button
bodyBody content
footerFooter content
closeSlotCustom close button

Props

PropTypeDefault
openbooleanfalse
titlestring-
descriptionstring-
overlaybooleantrue
scrollablebooleanfalse
fullscreenbooleanfalse
closeboolean | ClosePropstrue
dismissiblebooleantrue
transitionbooleantrue
portalbooleantrue
onOpenChange(open) => void-
refHTMLElement | nullnull
classstring-
uiRecord<Slot, Class>-