Dialog
A modal overlay for focused tasks or confirmations.
A modal overlay for focused tasks or confirmations, with a platform-adaptive action row.
Installation
npx touchcn add dialog
Usage
<tcn-dialog
[(open)]="open"
heading="Delete file?"
message="This action cannot be undone."
/>import { TcnDialog } from '@/components/ui/dialog';
import { TcnButton } from '@/components/ui/button';
<TcnDialog
open={open}
onOpenChange={setOpen}
heading="Delete file?"
message="This action cannot be undone."
actions={<TcnButton variant="destructive">Delete</TcnButton>}
/><script setup lang="ts">
import { TcnDialog } from '@/components/ui/dialog';
import { TcnButton } from '@/components/ui/button';
</script>
<template>
<TcnDialog
v-model:open="open"
heading="Delete file?"
message="This action cannot be undone."
>
<template #actions>
<TcnButton variant="destructive">Delete</TcnButton>
</template>
</TcnDialog>
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean |
false |
Open state (two-way bound on Angular). |
onOpenChange |
(open: boolean) => void |
— | Open-state callback (React). |
heading |
string |
'' |
Dialog heading. |
message |
string |
'' |
Body text. |
actions |
ReactNode |
— | Action row (typically TcnButtons) — React. |
Source · Angular
export * from './tcn-dialog';import { Component, computed, input, model, output } from '@angular/core';
import { TcnOverlayDirective } from '@touchcn/angular/overlay';
@Component({
selector: 'tcn-dialog',
imports: [TcnOverlayDirective],
template: `
<div
class="fixed inset-0 z-50 flex items-center justify-center p-6"
[class.pointer-events-none]="!open()"
[attr.data-state]="state()"
>
<div class="tcn-overlay-backdrop absolute inset-0 bg-black/40" (click)="overlay.dismiss()"></div>
<div
#overlay="tcnOverlay"
tcnOverlay
[(open)]="open"
(dismissed)="closed.emit()"
class="tcn-overlay-panel tcn-dialog-panel tcn-material relative w-full max-w-[300px] overflow-hidden rounded-[var(--radius-lg)] text-[var(--color-on-surface)]"
>
<div class="tcn-dialog-body px-5 pt-5 pb-4 text-center">
@if (heading()) {
<h2 class="text-[17px] font-semibold">{{ heading() }}</h2>
}
@if (message()) {
<p class="mt-1 text-sm text-[var(--color-on-surface-variant)]">{{ message() }}</p>
}
<ng-content />
</div>
<div class="tcn-dialog-actions">
<ng-content select="[dialog-action]" />
</div>
</div>
</div>
`,
})
export class TcnDialog {
readonly open = model(false);
readonly heading = input('');
readonly message = input('');
readonly closed = output<void>();
protected readonly state = computed(() => (this.open() ? 'open' : 'closed'));
}Source · React
export * from './tcn-dialog';import type { ReactNode } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { useOverlayPresence } from '@touchcn/react';
export interface TcnDialogProps {
open: boolean;
onOpenChange(open: boolean): void;
heading?: string;
message?: string;
/** Action row (typically `TcnButton`s); re-skinned per platform by CSS. */
actions?: ReactNode;
children?: ReactNode;
}
/**
* A modal overlay for focused tasks or confirmations. iOS splits the actions
* with hairlines (classic alert); MD right-aligns them. Built on Radix Dialog;
* the heading maps to `Dialog.Title` (rendered as an `<h2>`, matching Angular).
*/
export function TcnDialog({ open, onOpenChange, heading, message, actions, children }: TcnDialogProps) {
const { present, active, panelRef } = useOverlayPresence(open);
if (!present) {
return null;
}
const state = active ? 'open' : 'closed';
return (
<Dialog.Root open onOpenChange={(next) => !next && onOpenChange(false)}>
<Dialog.Portal>
<div className="fixed inset-0 z-50 flex items-center justify-center p-6" data-state={state}>
<Dialog.Overlay className="tcn-overlay-backdrop absolute inset-0 bg-black/40" />
<Dialog.Content
ref={panelRef}
aria-describedby={undefined}
className="tcn-overlay-panel tcn-dialog-panel tcn-material relative w-full max-w-[300px] overflow-hidden rounded-[var(--radius-lg)] text-[var(--color-on-surface)]"
>
<div className="tcn-dialog-body px-5 pt-5 pb-4 text-center">
{heading ? (
<Dialog.Title className="text-[17px] font-semibold">{heading}</Dialog.Title>
) : (
<Dialog.Title className="sr-only">Dialog</Dialog.Title>
)}
{message && <p className="mt-1 text-sm text-[var(--color-on-surface-variant)]">{message}</p>}
{children}
</div>
<div className="tcn-dialog-actions">{actions}</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
}Source · Vue
<script setup lang="ts">
import { computed } from 'vue';
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui';
import { useOverlayPresence } from '@touchcn/vue';
defineProps<{ heading?: string; message?: string }>();
/** Two-way bound open state (`v-model:open`). */
const open = defineModel<boolean>('open', { default: false });
const { present, active, setPanel } = useOverlayPresence(open);
const state = computed(() => (active.value ? 'open' : 'closed'));
const onRekaUpdate = (next: boolean): void => {
if (!next) {
open.value = false;
}
};
</script>
<template>
<DialogRoot v-if="present" :open="true" @update:open="onRekaUpdate">
<DialogPortal>
<div class="fixed inset-0 z-50 flex items-center justify-center p-6" :data-state="state">
<DialogOverlay class="tcn-overlay-backdrop absolute inset-0 bg-black/40" />
<DialogContent
:ref="setPanel"
:aria-describedby="undefined"
class="tcn-overlay-panel tcn-dialog-panel tcn-material relative w-full max-w-[300px] overflow-hidden rounded-[var(--radius-lg)] text-[var(--color-on-surface)]"
>
<div class="tcn-dialog-body px-5 pt-5 pb-4 text-center">
<DialogTitle v-if="heading" class="text-[17px] font-semibold">{{ heading }}</DialogTitle>
<DialogTitle v-else class="sr-only">Dialog</DialogTitle>
<p v-if="message" class="mt-1 text-sm text-[var(--color-on-surface-variant)]">{{ message }}</p>
<slot />
</div>
<div class="tcn-dialog-actions">
<slot name="actions" />
</div>
</DialogContent>
</div>
</DialogPortal>
</DialogRoot>
</template>export { default as TcnDialog } from './TcnDialog.vue';