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/overlayspnpm add @morphos/overlaysyarn add @morphos/overlaysbun add @morphos/overlaysCopy 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 { 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";/** * `Dialog` and `DialogTrigger` are re-exported directly from `@morphos/overlays` — their * `render()` is a no-op Fragment (`<>{children}</>` / a plain trigger button), and real usage * never mounts `Dialog` itself: you instantiate it directly (`@State() dialog = new Dialog()`) * and pass that instance to `DialogTrigger`/`DialogContent`/etc. as a prop. Wrapping it in a new * component class here would break `new Dialog()` — it would no longer have `.isOpen`, * `.openDialog()`, `.closeDialog()`. */export { Dialog, DialogTrigger, type DialogProps, type DialogTriggerProps } from "@morphos/overlays";/** * Morphos's `DialogContent` renders its own backdrop element (`[data-morphos-backdrop]`) — there * is no separate `DialogOverlay` part to compose. Style the backdrop globally instead: * * ```css * [data-morphos-backdrop] { * position: fixed; * inset: 0; * z-index: 50; * background: color-mix(in oklab, var(--foreground) 50%, transparent); * } * ``` */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" > ✕ </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/csspnpm add @morphos/overlays @praxisjs/cssyarn add @morphos/overlays @praxisjs/cssbun add @morphos/overlays @praxisjs/cssCopy 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 { 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 });}/** * `Dialog` and `DialogTrigger` are re-exported directly from `@morphos/overlays` — their * `render()` is a no-op Fragment (`<>{children}</>` / a plain trigger button), and real usage * never mounts `Dialog` itself: you instantiate it directly (`@State() dialog = new Dialog()`) * and pass that instance to `DialogTrigger`/`DialogContent`/etc. as a prop. Wrapping it in a new * component class here would break `new Dialog()` — it would no longer have `.isOpen`, * `.openDialog()`, `.closeDialog()`. */export { Dialog, DialogTrigger, type DialogProps, type DialogTriggerProps } from "@morphos/overlays";/** * Morphos's `DialogContent` renders its own backdrop element (`[data-morphos-backdrop]`) — there * is no separate `DialogOverlay` part to compose. The theme module's `globalStyle()` call already * styles that backdrop globally (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}> ✕ </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 (✕) 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.