Skip to content
Components

Dialog

A modal or non-modal surface for focused tasks that need supporting context.

Playground

modal

Traps focus and makes background content inert while open.

Usage

tsx
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogExitButton,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "earthling-ui/dialog";

export function Example() {
  return (
    <>
      <Dialog>
        <DialogTrigger>Edit profile</DialogTrigger>
        <DialogContent>
          <DialogExitButton />
          <DialogHeader>
            <DialogTitle>Edit profile</DialogTitle>
            <DialogDescription>
              Update the details shown to your workspace.
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <DialogClose>Cancel</DialogClose>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}
  • Give every dialog a visible title and description so assistive technology can announce its purpose.
  • Keep the primary action in the footer and return focus to the trigger when the dialog closes.
  • Use non-modal dialogs sparingly because background content remains interactive.

Installation

Start with the project setup for React and Tailwind CSS 4. Both paths use the same component and theme.

bash
npm install earthling-ui

Use the component import shown above. Package updates keep the implementation current.

API reference

Declarations from the current library build, including inherited primitive props. Playground controls above show selected options; they are not the complete API.

tsx
// Generated by dts-bundle-generator v9.5.1

import * as DialogPrimitive from '@radix-ui/react-dialog';
import { ComponentProps } from 'react';

export type DialogContextValue = {
	isOpen: boolean;
	close: () => void;
};
export declare const useDialog: () => DialogContextValue;
export declare const Dialog: ({ open, defaultOpen, onOpenChange, ...rest }: ComponentProps<typeof DialogPrimitive.Root>) => import("react").JSX.Element;
export declare const DialogTrigger: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
export declare const DialogPortal: import("react").FC<DialogPrimitive.DialogPortalProps>;
export declare const DialogClose: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
export declare const DialogOverlay: import("react").ForwardRefExoticComponent<Omit<DialogPrimitive.DialogOverlayProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
export declare const DialogContent: import("react").ForwardRefExoticComponent<Omit<DialogPrimitive.DialogContentProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
export declare const DialogExitButton: import("react").ForwardRefExoticComponent<Omit<DialogPrimitive.DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
export declare const DialogHeader: {
	({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): import("react").JSX.Element;
	displayName: string;
};
export declare const DialogFooter: {
	({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): import("react").JSX.Element;
	displayName: string;
};
export declare const DialogTitle: import("react").ForwardRefExoticComponent<Omit<DialogPrimitive.DialogTitleProps & import("react").RefAttributes<HTMLHeadingElement>, "ref"> & import("react").RefAttributes<HTMLHeadingElement>>;
export declare const DialogDescription: import("react").ForwardRefExoticComponent<Omit<DialogPrimitive.DialogDescriptionProps & import("react").RefAttributes<HTMLParagraphElement>, "ref"> & import("react").RefAttributes<HTMLParagraphElement>>;
export declare const DialogForm: import("react").ForwardRefExoticComponent<Omit<import("react").ClassAttributes<HTMLFormElement> & import("react").FormHTMLAttributes<HTMLFormElement> & {
	asChild?: boolean;
}, "ref"> & import("react").RefAttributes<HTMLFormElement>>;

export {};

Runtime dependencies: @radix-ui/react-dialog, @radix-ui/react-slot, cnfast.

Source

Use the eject command to copy this implementation with its required helpers and project-specific imports.

View Dialog implementation
tsx
"use client";

import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "@/utils/cn";
import {
  createContext,
  forwardRef,
  useContext,
  useState,
  type ComponentProps,
  type ComponentPropsWithoutRef,
  type ComponentRef,
} from "react";
import { Slot } from "@radix-ui/react-slot";

type DialogContextValue = {
  isOpen: boolean;
  close: () => void;
};
const DialogContext = createContext<DialogContextValue>({
  isOpen: false,
  close: () => {},
});
const useDialog = () => useContext(DialogContext);

const Dialog = ({
  open,
  defaultOpen,
  onOpenChange,
  ...rest
}: ComponentProps<typeof DialogPrimitive.Root>) => {
  const [openState, setOpenState] = useState(defaultOpen || false);
  const isOpen = open === undefined ? openState : open;
  return (
    <DialogContext.Provider
      value={{
        isOpen,
        close: () => {
          setOpenState(false);
          onOpenChange?.(false);
        },
      }}
    >
      <DialogPrimitive.Root
        {...rest}
        defaultOpen={defaultOpen}
        open={isOpen}
        onOpenChange={(isOpen) => {
          setOpenState(isOpen);
          onOpenChange?.(isOpen);
        }}
      />
    </DialogContext.Provider>
  );
};

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = DialogPrimitive.Portal;

const DialogClose = DialogPrimitive.Close;

const DialogOverlay = forwardRef<
  ComponentRef<typeof DialogPrimitive.Overlay>,
  ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      "fixed inset-0 z-50 bg-black/80 duration-150 ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 motion-reduce:animate-none",
      className,
    )}
    {...props}
  />
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = forwardRef<
  ComponentRef<typeof DialogPrimitive.Content>,
  ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay />
    <DialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed start-1/2 top-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-xl border bg-background p-6 shadow-lg duration-200 ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 motion-reduce:animate-none",
        className,
      )}
      {...props}
    >
      {children}
    </DialogPrimitive.Content>
  </DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogExitButton = forwardRef<
  ComponentRef<typeof DialogPrimitive.Close>,
  ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Close
    ref={ref}
    className={cn(
      "absolute end-2 top-2 flex size-10 cursor-pointer items-center justify-center rounded-lg opacity-70 ring-offset-background hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-outline focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-muted data-[state=open]:text-muted-foreground",
      className,
    )}
    {...props}
  >
    <i className="block icon-[lucide--x]" aria-hidden="true" />
    <span className="sr-only">Close</span>
  </DialogPrimitive.Close>
));
DialogExitButton.displayName = "DialogExitButton";

const DialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-1.5 text-center sm:text-left",
      className,
    )}
    {...props}
  />
);
DialogHeader.displayName = "DialogHeader";

const DialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className,
    )}
    {...props}
  />
);
DialogFooter.displayName = "DialogFooter";

const DialogTitle = forwardRef<
  ComponentRef<typeof DialogPrimitive.Title>,
  ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Title
    ref={ref}
    className={cn(
      "text-lg font-semibold leading-none tracking-tight text-balance",
      className,
    )}
    {...props}
  />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = forwardRef<
  ComponentRef<typeof DialogPrimitive.Description>,
  ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground text-pretty", className)}
    {...props}
  />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

const DialogForm = forwardRef<
  ComponentRef<"form">,
  ComponentProps<"form"> & { asChild?: boolean }
>(({ action, asChild, ...props }, ref) => {
  const dialog = useDialog();
  const Comp = asChild ? Slot : "form";
  return (
    <Comp
      ref={ref}
      {...props}
      action={
        typeof action === "function"
          ? async (formData) => {
              await action(formData);
              dialog.close();
            }
          : action
      }
    />
  );
});
DialogForm.displayName = "DialogForm";

export {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogExitButton,
  DialogFooter,
  DialogForm,
  DialogHeader,
  DialogOverlay,
  DialogPortal,
  DialogTitle,
  DialogTrigger,
  useDialog,
};