Components
Dialog
A modal dialog with focus trap and scroll lock, wrapping Morphos's Dialog primitive.
Installation
npx kosmesis add dialogpnpm dlx kosmesis add dialogyarn dlx kosmesis add dialogbunx kosmesis add dialogInstall the following dependencies:
npm install @morphos/overlays @morphos/iconspnpm add @morphos/overlays @morphos/iconsyarn add @morphos/overlays @morphos/iconsbun add @morphos/overlays @morphos/iconsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { DialogClose as MorphosDialogClose, DialogContent as MorphosDialogContent, DialogDescription as MorphosDialogDescription, DialogTitle as MorphosDialogTitle, type DialogCloseProps as MorphosDialogCloseProps, type DialogContentProps as MorphosDialogContentProps, type DialogDescriptionProps as MorphosDialogDescriptionProps, type DialogTitleProps as MorphosDialogTitleProps} from "@morphos/overlays";import { cn } from "@/lib/utils";// Re-exported directly: `Dialog` is instantiated directly (`@State() dialog = new Dialog()`), not// mounted via JSX, so wrapping it here would break `.isOpen`/`.openDialog()`/`.closeDialog()`.export { Dialog, DialogTrigger, type DialogProps, type DialogTriggerProps } from "@morphos/overlays";// `DialogContent` renders its own backdrop (`[data-morphos-backdrop]`) — there is no separate// `DialogOverlay` part; style the backdrop globally.export interface DialogContentProps extends MorphosDialogContentProps { showCloseButton?: boolean;}@Component()export class DialogContent extends StatelessComponent<DialogContentProps> { render() { const { class: cls, children, showCloseButton = true, dialog, ...rest } = this.props; return ( <MorphosDialogContent dialog={dialog} class={cn( "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg outline-none sm:max-w-lg", "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", cls, )} {...rest} > {children} {showCloseButton && ( <DialogClose dialog={dialog} class="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0" > <Icon name="X" size={16} /> </DialogClose> )} </MorphosDialogContent> ); }}export interface DialogHeaderProps { class?: string; children?: Children;}@Component()export class DialogHeader extends StatelessComponent<DialogHeaderProps> { render() { const { class: cls, children } = this.props; return <div class={cn("flex flex-col gap-2 text-center sm:text-left", cls)}>{children}</div>; }}export interface DialogFooterProps { class?: string; children?: Children;}@Component()export class DialogFooter extends StatelessComponent<DialogFooterProps> { render() { const { class: cls, children } = this.props; return ( <div class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", cls)}>{children}</div> ); }}export type DialogTitleProps = MorphosDialogTitleProps;@Component()export class DialogTitle extends StatelessComponent<DialogTitleProps> { render() { const { class: cls, ...rest } = this.props; return <MorphosDialogTitle class={cn("text-lg leading-none font-semibold", cls)} {...rest} />; }}export type DialogDescriptionProps = MorphosDialogDescriptionProps;@Component()export class DialogDescription extends StatelessComponent<DialogDescriptionProps> { render() { const { class: cls, ...rest } = this.props; return <MorphosDialogDescription class={cn("text-sm text-muted-foreground", cls)} {...rest} />; }}export type DialogCloseProps = MorphosDialogCloseProps;@Component()export class DialogClose extends StatelessComponent<DialogCloseProps> { render() { return <MorphosDialogClose {...this.props} />; }}Install the following dependencies:
npm install @morphos/overlays @praxisjs/css @morphos/iconspnpm add @morphos/overlays @praxisjs/css @morphos/iconsyarn add @morphos/overlays @praxisjs/css @morphos/iconsbun add @morphos/overlays @praxisjs/css @morphos/iconsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, keyframes, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { DialogClose as MorphosDialogClose, DialogContent as MorphosDialogContent, DialogDescription as MorphosDialogDescription, DialogTitle as MorphosDialogTitle, type DialogCloseProps as MorphosDialogCloseProps, type DialogContentProps as MorphosDialogContentProps, type DialogDescriptionProps as MorphosDialogDescriptionProps, type DialogTitleProps as MorphosDialogTitleProps} from "@morphos/overlays";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);const popIn = keyframes("kosmesis-pop-in", { from: { opacity: "0", transform: "translate(-50%, -50%) scale(0.95)" }, to: { opacity: "1", transform: "translate(-50%, -50%) scale(1)" },});class DialogStyles extends Stylesheet { $content = this.css({ position: "fixed", top: "50%", left: "50%", zIndex: 50, display: "grid", width: "100%", maxWidth: "calc(100% - 2rem)", transform: "translate(-50%, -50%)", gap: "1rem", borderRadius: "0.5rem", border: `1px solid ${t.border}`, backgroundColor: t.background, padding: "1.5rem", boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)", outline: "none", }) .media("(min-width: 640px)", { maxWidth: "32rem" }) .on("&[data-open]", { animation: `${popIn} 150ms ease-out` }); $close = this.css({ position: "absolute", top: "1rem", right: "1rem", borderRadius: "2px", opacity: 0.7, transition: "opacity 120ms ease", }) .hover({ opacity: 1 }) .disabled({ pointerEvents: "none" }) .on("& svg", { width: "1rem", height: "1rem" }); $header = this.css({ display: "flex", flexDirection: "column", gap: "0.5rem", textAlign: "center" }).media( "(min-width: 640px)", { textAlign: "left" }, ); $footer = this.css({ display: "flex", flexDirection: "column-reverse", gap: "0.5rem" }).media("(min-width: 640px)", { flexDirection: "row", justifyContent: "flex-end", }); $title = this.css({ fontSize: "1.125rem", lineHeight: 1, fontWeight: 600 }); $description = this.css({ fontSize: "0.875rem", color: t.mutedForeground });}// Re-exported directly: `Dialog` is instantiated directly (`@State() dialog = new Dialog()`), not// mounted via JSX, so wrapping it here would break `.isOpen`/`.openDialog()`/`.closeDialog()`.export { Dialog, DialogTrigger, type DialogProps, type DialogTriggerProps } from "@morphos/overlays";// `DialogContent` renders its own backdrop (`[data-morphos-backdrop]`) — there is no separate// `DialogOverlay` part; the theme module's `globalStyle()` call styles it (see `@/lib/kosmesis-theme`).export interface DialogContentProps extends MorphosDialogContentProps { showCloseButton?: boolean;}@Component()export class DialogContent extends StatelessComponent<DialogContentProps> { @Styled(DialogStyles) $s!: DialogStyles; render() { const { class: cls, children, showCloseButton = true, dialog, ...rest } = this.props; return ( <MorphosDialogContent dialog={dialog} class={cx(this.$s.$content, cls)} {...rest}> {children} {showCloseButton && ( <DialogClose dialog={dialog} class={this.$s.$close}> <Icon name="X" size={16} /> </DialogClose> )} </MorphosDialogContent> ); }}export interface DialogHeaderProps { class?: string; children?: Children;}@Component()export class DialogHeader extends StatelessComponent<DialogHeaderProps> { @Styled(DialogStyles) $s!: DialogStyles; render() { const { class: cls, children } = this.props; return <div class={cx(this.$s.$header, cls)}>{children}</div>; }}export interface DialogFooterProps { class?: string; children?: Children;}@Component()export class DialogFooter extends StatelessComponent<DialogFooterProps> { @Styled(DialogStyles) $s!: DialogStyles; render() { const { class: cls, children } = this.props; return <div class={cx(this.$s.$footer, cls)}>{children}</div>; }}export type DialogTitleProps = MorphosDialogTitleProps;@Component()export class DialogTitle extends StatelessComponent<DialogTitleProps> { @Styled(DialogStyles) $s!: DialogStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosDialogTitle class={cx(this.$s.$title, cls)} {...rest} />; }}export type DialogDescriptionProps = MorphosDialogDescriptionProps;@Component()export class DialogDescription extends StatelessComponent<DialogDescriptionProps> { @Styled(DialogStyles) $s!: DialogStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosDialogDescription class={cx(this.$s.$description, cls)} {...rest} />; }}export type DialogCloseProps = MorphosDialogCloseProps;@Component()export class DialogClose extends StatelessComponent<DialogCloseProps> { render() { return <MorphosDialogClose {...this.props} />; }}Examples
Usage
import { Button, buttonVariants } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
@Component()
class EditProfileDialog extends StatefulComponent {
@State() dialog = new Dialog();
render() {
return (
<>
<DialogTrigger dialog={this.dialog} class={buttonVariants({ variant: "outline" })}>
Edit profile
</DialogTrigger>
<DialogContent dialog={this.dialog} aria-labelledby="edit-title">
<DialogHeader>
<DialogTitle id="edit-title">Edit profile</DialogTitle>
<DialogDescription>Make changes to your profile here.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose dialog={this.dialog} class={buttonVariants({ variant: "outline" })}>
Cancel
</DialogClose>
<Button onClick={() => { this.dialog.closeDialog(); }}>Save changes</Button>
</DialogFooter>
</DialogContent>
</>
);
}
}DialogContent renders a close (X icon) button in the top-right corner by default — pass
showCloseButton={false} to omit it. Dialog is never mounted via JSX; it's instantiated
directly (@State() dialog = new Dialog()) and passed to DialogTrigger/DialogContent/etc.
Props
Dialog
Re-exported directly from @morphos/overlays — instantiate it in state (@State() dialog = new Dialog()) and pass it to every part below.
| Prop | Type | Default |
|---|---|---|
open | boolean | — controlled |
defaultOpen | boolean | — |
onOpenChange | (open: boolean) => void | — |
closeOnEscape | boolean | true |
closeOnBackdropClick | boolean | true |
DialogTrigger
| Prop | Type | Default |
|---|---|---|
dialog | Dialog | — required |
DialogContent
| Prop | Type | Default |
|---|---|---|
dialog | Dialog | — required |
aria-label | string | — |
aria-labelledby | string | — |
aria-describedby | string | — |
showCloseButton | boolean | true |
DialogTitle
| Prop | Type | Default |
|---|---|---|
as | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | — |
DialogClose
| Prop | Type | Default |
|---|---|---|
dialog | Dialog | — required |
DialogHeader, DialogFooter, and DialogDescription take no props beyond class/children.