Kosmesis
Components

Dialog

A modal dialog with focus trap and scroll lock, wrapping Morphos's Dialog primitive.

Installation

npx kosmesis add dialog
pnpm dlx kosmesis add dialog
yarn dlx kosmesis add dialog
bunx kosmesis add dialog

Install the following dependencies:

npm install @morphos/overlays
pnpm add @morphos/overlays
yarn add @morphos/overlays
bun add @morphos/overlays

Copy and paste the following code into your project.

dialog.tsx
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/css
pnpm add @morphos/overlays @praxisjs/css
yarn add @morphos/overlays @praxisjs/css
bun add @morphos/overlays @praxisjs/css

Copy and paste the following code into your project.

dialog.tsx
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.

Props

Dialog

Re-exported directly from @morphos/overlays — instantiate it in state (@State() dialog = new Dialog()) and pass it to every part below.

PropTypeDefault
openboolean— controlled
defaultOpenboolean
onOpenChange(open: boolean) => void
closeOnEscapebooleantrue
closeOnBackdropClickbooleantrue

DialogTrigger

PropTypeDefault
dialogDialog— required

DialogContent

PropTypeDefault
dialogDialog— required
aria-labelstring
aria-labelledbystring
aria-describedbystring
showCloseButtonbooleantrue

DialogTitle

PropTypeDefault
as"h1" | "h2" | "h3" | "h4" | "h5" | "h6"

DialogClose

PropTypeDefault
dialogDialog— required

DialogHeader, DialogFooter, and DialogDescription take no props beyond class/children.

On this page