Dialog
A modal or non-modal surface for focused tasks that need supporting context.
Preview
Example code
Playground
modalTraps 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.
Import package
Own the source
bash
npm install earthling-uiUse 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,
};