diff --git a/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-api.tsx b/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-api.tsx
index 1d6a147687..74968ecc42 100644
--- a/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-api.tsx
+++ b/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-api.tsx
@@ -1,10 +1,10 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
import { toast } from "@/components/ui/toaster";
import { formatNumber } from "@/lib/fmt";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Lock } from "@unkey/icons";
+import { DialogContainer } from "@unkey/ui";
import { Button, Input, SettingCard } from "@unkey/ui";
import { useRouter } from "next/navigation";
import type React from "react";
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-protection.tsx b/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-protection.tsx
index d504ddb351..49cdcd3ad2 100644
--- a/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-protection.tsx
+++ b/apps/dashboard/app/(app)/apis/[apiId]/settings/components/delete-protection.tsx
@@ -1,9 +1,9 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowUpRight, TriangleWarning2 } from "@unkey/icons";
+import { DialogContainer } from "@unkey/ui";
import { InlineLink, Input, SettingCard } from "@unkey/ui";
import { Button } from "@unkey/ui";
import { useRouter } from "next/navigation";
diff --git a/apps/dashboard/app/(app)/authorization/_components/rbac-form.tsx b/apps/dashboard/app/(app)/authorization/_components/rbac-form.tsx
index 2e65ecb761..e73ab48c61 100644
--- a/apps/dashboard/app/(app)/authorization/_components/rbac-form.tsx
+++ b/apps/dashboard/app/(app)/authorization/_components/rbac-form.tsx
@@ -1,10 +1,10 @@
"use client";
import { revalidateTag } from "@/app/actions";
-import { DialogContainer } from "@/components/dialog-container";
import { toast } from "@/components/ui/toaster";
import { tags } from "@/lib/cache";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
+import { DialogContainer } from "@unkey/ui";
import { Button, FormInput, FormTextarea } from "@unkey/ui";
import { validation } from "@unkey/validation";
import { useRouter } from "next/navigation";
diff --git a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx
index fa7e42eca2..1a52941176 100644
--- a/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx
+++ b/apps/dashboard/app/(app)/authorization/permissions/[permissionId]/delete-permission.tsx
@@ -1,9 +1,9 @@
"use client";
import { revalidate } from "@/app/actions";
-import { DialogContainer } from "@/components/dialog-container";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
+import { DialogContainer } from "@unkey/ui";
import { Button, Input } from "@unkey/ui";
import { useRouter } from "next/navigation";
import { useState } from "react";
diff --git a/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx b/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx
index 4711fa8417..69f92bf0c6 100644
--- a/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx
+++ b/apps/dashboard/app/(app)/authorization/roles/[roleId]/delete-role.tsx
@@ -1,8 +1,8 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
+import { DialogContainer } from "@unkey/ui";
import { Button, Input } from "@unkey/ui";
import { useRouter } from "next/navigation";
import { useState } from "react";
diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/delete-dialog.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/delete-dialog.tsx
index 5473c873c0..2adff0d945 100644
--- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/delete-dialog.tsx
+++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/delete-dialog.tsx
@@ -1,9 +1,9 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
+import { DialogContainer } from "@unkey/ui";
import { Button, Input } from "@unkey/ui";
import type { PropsWithChildren } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/identifier-dialog.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/identifier-dialog.tsx
index 508f65b656..d7f3c8f345 100644
--- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/identifier-dialog.tsx
+++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/identifier-dialog.tsx
@@ -1,13 +1,20 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
import { Badge } from "@/components/ui/badge";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircleInfo } from "@unkey/icons";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@unkey/ui";
-import { Button, FormInput } from "@unkey/ui";
+import {
+ Button,
+ DialogContainer,
+ FormInput,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@unkey/ui";
import type { PropsWithChildren } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/namespace-delete-dialog.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/namespace-delete-dialog.tsx
index 70f444ab8a..0c0476b5ca 100644
--- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/namespace-delete-dialog.tsx
+++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/namespace-delete-dialog.tsx
@@ -1,11 +1,11 @@
"use client";
import { revalidateTag } from "@/app/actions";
-import { DialogContainer } from "@/components/dialog-container";
import { toast } from "@/components/ui/toaster";
import { tags } from "@/lib/cache";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
+import { DialogContainer } from "@unkey/ui";
import { Button, Input } from "@unkey/ui";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
diff --git a/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx b/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx
index 456eb0d26d..f37412e59c 100644
--- a/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx
+++ b/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx
@@ -1,11 +1,11 @@
"use client";
import { revalidate } from "@/app/actions";
-import { DialogContainer } from "@/components/dialog-container";
import { NavbarActionButton } from "@/components/navigation/action-button";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
+import { DialogContainer } from "@unkey/ui";
import { Button, FormInput } from "@unkey/ui";
import { Plus } from "lucide-react";
import { useRouter } from "next/navigation";
diff --git a/apps/dashboard/app/(app)/settings/billing/components/confirmation.tsx b/apps/dashboard/app/(app)/settings/billing/components/confirmation.tsx
index caa205ec08..aaa20eaea1 100644
--- a/apps/dashboard/app/(app)/settings/billing/components/confirmation.tsx
+++ b/apps/dashboard/app/(app)/settings/billing/components/confirmation.tsx
@@ -1,6 +1,6 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
+import { DialogContainer } from "@unkey/ui";
import { Button } from "@unkey/ui";
import { useState } from "react";
diff --git a/apps/dashboard/app/(app)/settings/team/invite.tsx b/apps/dashboard/app/(app)/settings/team/invite.tsx
index 8691191bdc..09c2a09bca 100644
--- a/apps/dashboard/app/(app)/settings/team/invite.tsx
+++ b/apps/dashboard/app/(app)/settings/team/invite.tsx
@@ -1,5 +1,4 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
import {
Form,
FormControl,
@@ -14,8 +13,15 @@ import { toast } from "@/components/ui/toaster";
import type { AuthenticatedUser, Organization } from "@/lib/auth/types";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@unkey/ui";
-import { Button } from "@unkey/ui";
+import {
+ Button,
+ DialogContainer,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@unkey/ui";
import { Plus } from "lucide-react";
import type React from "react";
import { useState } from "react";
diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx
index 81eb794bf3..fd7dd46918 100644
--- a/apps/dashboard/app/auth/sign-in/org-selector.tsx
+++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx
@@ -1,6 +1,6 @@
"use client";
-import { DialogContainer } from "@/components/dialog-container";
+import { DialogContainer } from "@unkey/ui";
import type { Organization } from "@/lib/auth/types";
import { Button } from "@unkey/ui";
diff --git a/apps/engineering/content/design/components/dialog-container.example.tsx b/apps/engineering/content/design/components/dialog-container.example.tsx
new file mode 100644
index 0000000000..b13088f160
--- /dev/null
+++ b/apps/engineering/content/design/components/dialog-container.example.tsx
@@ -0,0 +1,63 @@
+"use client";
+import { RenderComponentWithSnippet } from "@/app/components/render";
+import { DialogContainer } from "@unkey/ui";
+import { Button, Input } from "@unkey/ui";
+import { useState } from "react";
+
+export function DialogContainerExample() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+ const [inputResult, setInputResult] = useState("");
+
+ const handleSubmit = () => {
+ setInputResult(inputValue);
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+
+
+ setIsOpen(!isOpen)}
+ subTitle="This is an example of a subTitle. Normally used to describe the dialog"
+ title="Example Dialog Title"
+ footer={
+
+
+
+ This is an example of a footer with a button for actions needed to be done
+
+
+ }
+ >
+
+
Dialog Content
+ setInputValue(e.target.value)}
+ />
+
+
+
+ Input Result: {inputResult}
+
+
+
+
+ );
+}
diff --git a/apps/engineering/content/design/components/dialog-container.mdx b/apps/engineering/content/design/components/dialog-container.mdx
new file mode 100644
index 0000000000..5b353ff998
--- /dev/null
+++ b/apps/engineering/content/design/components/dialog-container.mdx
@@ -0,0 +1,36 @@
+---
+title: Dialog Container
+---
+import { DialogContainerExample } from "./dialog-container.example"
+
+## Dialog Container
+
+The Dialog Container is a flexible modal component that provides a consistent way to display content in a modal dialog. It's built on top of Radix UI's Dialog primitive with additional styling and functionality.
+
+### Features
+
+- Accessible modal implementation
+- Customizable overlay and content styling
+- Close button with warning support
+- Keyboard navigation support
+- Customizable animations
+- Responsive design
+
+### Usage
+
+
+
+### Props
+
+| Prop | Type | Default | Description |
+|-------------------|-------------------------|-----------|--------------------------------------------------|
+| isOpen | boolean | - | Controls the open state of the dialog |
+| onOpenChange | (value: boolean) => void | - | Callback when the open state changes |
+| title | string | - | The title of the dialog |
+| subTitle | string | - | Optional subtitle for the dialog |
+| footer | ReactNode | - | Optional footer content |
+| className | string | - | Additional classes for the dialog container |
+| contentClassName | string | - | Additional classes for the dialog content |
+| preventAutoFocus | boolean | false | Whether to prevent auto-focus on open |
+| children | ReactNode | - | The content to display in the dialog |
+
diff --git a/internal/ui/package.json b/internal/ui/package.json
index b4a28ad273..f261e9e515 100644
--- a/internal/ui/package.json
+++ b/internal/ui/package.json
@@ -16,6 +16,7 @@
},
"dependencies": {
"@radix-ui/colors": "^3.0.0",
+ "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.0.7",
diff --git a/internal/ui/src/components/dialog/dialog-container.tsx b/internal/ui/src/components/dialog/dialog-container.tsx
new file mode 100644
index 0000000000..54aba4b573
--- /dev/null
+++ b/internal/ui/src/components/dialog/dialog-container.tsx
@@ -0,0 +1,52 @@
+"use client";
+import type { PropsWithChildren, ReactNode } from "react";
+// biome-ignore lint: React in this context is used throughout, so biome will change to types because no APIs are used even though React is needed.
+import React from "react";
+import { cn } from "../../lib/utils";
+import { Dialog, DialogContent } from "./dialog";
+import { DefaultDialogContentArea, DefaultDialogFooter, DefaultDialogHeader } from "./dialog-parts";
+
+type DialogContainerProps = PropsWithChildren<{
+ className?: string;
+ isOpen: boolean;
+ onOpenChange: (value: boolean) => void;
+ title: string;
+ footer?: ReactNode;
+ contentClassName?: string;
+ preventAutoFocus?: boolean;
+ subTitle?: string;
+}>;
+
+export const DialogContainer = ({
+ className,
+ isOpen,
+ subTitle,
+ onOpenChange,
+ title,
+ children,
+ footer,
+ contentClassName,
+ preventAutoFocus = true,
+}: DialogContainerProps) => {
+ return (
+
+ );
+};
+
+export { DefaultDialogHeader, DefaultDialogContentArea, DefaultDialogFooter };
diff --git a/internal/ui/src/components/dialog/dialog-parts.tsx b/internal/ui/src/components/dialog/dialog-parts.tsx
new file mode 100644
index 0000000000..59b18cd975
--- /dev/null
+++ b/internal/ui/src/components/dialog/dialog-parts.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+// biome-ignore lint: React in this context is used throughout, so biome will change to types because no APIs are used even though React is needed.
+import * as React from "react";
+import type { PropsWithChildren } from "react";
+import { cn } from "../../lib/utils";
+import {
+ DialogFooter as ShadcnDialogFooter,
+ DialogHeader as ShadcnDialogHeader,
+ DialogTitle as ShadcnDialogTitle,
+} from "./dialog";
+
+type DefaultDialogHeaderProps = {
+ title: string;
+ subTitle?: string;
+ className?: string;
+};
+
+export const DefaultDialogHeader = ({ title, subTitle, className }: DefaultDialogHeaderProps) => {
+ return (
+
+
+ {title}
+ {subTitle && ( // Conditionally render subtitle span only if it exists
+ {subTitle}
+ )}
+
+
+ );
+};
+
+type DefaultDialogContentAreaProps = PropsWithChildren<{
+ className?: string;
+}>;
+
+export const DefaultDialogContentArea = ({
+ children,
+ className,
+}: DefaultDialogContentAreaProps) => {
+ return (
+