Skip to content

Instantly share code, notes, and snippets.

@nemedib
Last active February 14, 2026 10:11
Show Gist options
  • Select an option

  • Save nemedib/a6ecf16caf34a634450ec72599bdb8be to your computer and use it in GitHub Desktop.

Select an option

Save nemedib/a6ecf16caf34a634450ec72599bdb8be to your computer and use it in GitHub Desktop.
Typed async dialog store for Svelte shadcn dialogs. open() returns a Promise inferred from the dialog component’s close(result) prop. Includes a DialogHost renderer and example usage.
<script lang="ts">
import { DialogStore } from "$lib/context/dialog-store.svelte";
import DialogHost from "$lib/components/ui/dialog/dialog-host.svelte";
import { setContext } from "svelte";
setContext("dialog", new DialogStore());
</script>
<DialogHost />
<script lang="ts">
import AreYouSureDialog from "$lib/components/dialogs/are-you-sure-dialog.svelte";
import { DialogStore } from "$lib/context/dialog-store.svelte";
import { getContext } from "svelte";
const dialogStore = getContext<DialogStore>("dialog");
async function onClick() {
const result = await dialogStore.open(AreYouSureDialog, {
title: "Are you sure?",
description: "...",
});
if (!result) return;
alert("You are sure!")
}
</script>
<button onclick={onClick}>
Click me
</button>
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog/index.js";
import Button from "../ui/button/button.svelte";
type Props = {
close: (data: boolean) => void;
title: string;
description: string;
};
const { close, title, description }: Props = $props();
</script>
<Dialog.Content interactOutsideBehavior="ignore">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>{description}</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Button onclick={() => close(true)}>
Ok
</Button>
<Button variant="ghost" onclick={() => close(false)}>
Cancel
</Button>
</Dialog.Footer>
</Dialog.Content>
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog";
import { DialogStore } from "$lib/context/dialog-store.svelte";
import { getContext } from "svelte";
const dialogStore = getContext<DialogStore>("dialog");
let top = $derived(dialogStore.stack.at(-1));
</script>
{#if top}
<Dialog.Root
open={true}
onOpenChange={(open) => {
if (!open) dialogStore.close(top.id, undefined);
}}
>
<top.component {...top.props} close={(result: any) => dialogStore.close(top.id, result)} />
</Dialog.Root>
{/if}
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Component, type ComponentProps } from "svelte";
export type DialogResult<C extends Component<any>> =
ComponentProps<C> extends { close?: (result: infer R) => any } ? R | undefined : void;
export type DialogPublicProps<C extends Component<any>> =
Omit<ComponentProps<C>, "close">;
type AnyComponent = Component<any>;
export type DialogConfig = { closeOnClickOutside?: boolean }
type Entry<C extends AnyComponent> = {
id: string;
component: C;
props?: DialogPublicProps<C>;
config?: DialogConfig
resolve: (value: DialogResult<C>) => void;
};
export class DialogStore {
private _stack = $state<Entry<any>[]>([]);
get stack() {
return this._stack;
}
open<C extends AnyComponent>(
component: C,
props?: DialogPublicProps<C>,
config?: DialogConfig
): Promise<DialogResult<C>> {
return new Promise((resolve) => {
const id = crypto.randomUUID();
const entry: Entry<C> = { id, component, config, props, resolve };
this._stack.push(entry);
});
}
close<C extends AnyComponent>(id: number, result: DialogResult<C>) {
const indexToDelete = this._stack.findIndex((x) => x.id === id)
if (indexToDelete >= 0) {
this._stack[indexToDelete].resolve(result);
this._stack.splice(indexToDelete, 1)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment