diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.js b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.js
new file mode 100644
index 0000000000..6c34a8e923
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.js
@@ -0,0 +1,117 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { useTheme } from '@mui/system';
+
+export default function AlertDialogIntroduction() {
+ return (
+
+
+ Subscribe
+
+
+ Subscribe
+
+ Are you sure you want to subscribe?
+
+
+
+
+
+
+ );
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+function Styles() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.tsx b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.tsx
new file mode 100644
index 0000000000..6c34a8e923
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.tsx
@@ -0,0 +1,117 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { useTheme } from '@mui/system';
+
+export default function AlertDialogIntroduction() {
+ return (
+
+
+ Subscribe
+
+
+ Subscribe
+
+ Are you sure you want to subscribe?
+
+
+
+
+
+
+ );
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+function Styles() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.js b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.js
new file mode 100644
index 0000000000..6f2f0907cd
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.js
@@ -0,0 +1,104 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { styled } from '@mui/system';
+
+export default function AlertDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+ Are you sure you want to subscribe?
+
+ Yes
+ No
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const TriggerButton = styled(AlertDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family: "IBM Plex Sans", sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Popup = styled(AlertDialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: "IBM Plex Sans", sans-serif;
+ transform: translate(-50%, -50%);
+ padding: 16px;
+ z-index: 2100;
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
+
+const CloseButton = styled(AlertDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: "IBM Plex Sans", sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Title = styled(AlertDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Description = styled(AlertDialog.Description)``;
+
+const Backdrop = styled(AlertDialog.Backdrop)`
+ background: rgb(0 0 0 / 0.35);
+ position: fixed;
+ inset: 0;
+ backdrop-filter: blur(4px);
+ z-index: 2000;
+`;
diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx
new file mode 100644
index 0000000000..6f2f0907cd
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx
@@ -0,0 +1,104 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { styled } from '@mui/system';
+
+export default function AlertDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+ Are you sure you want to subscribe?
+
+ Yes
+ No
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const TriggerButton = styled(AlertDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family: "IBM Plex Sans", sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Popup = styled(AlertDialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: "IBM Plex Sans", sans-serif;
+ transform: translate(-50%, -50%);
+ padding: 16px;
+ z-index: 2100;
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
+
+const CloseButton = styled(AlertDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: "IBM Plex Sans", sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Title = styled(AlertDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Description = styled(AlertDialog.Description)``;
+
+const Backdrop = styled(AlertDialog.Backdrop)`
+ background: rgb(0 0 0 / 0.35);
+ position: fixed;
+ inset: 0;
+ backdrop-filter: blur(4px);
+ z-index: 2000;
+`;
diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx.preview b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx.preview
new file mode 100644
index 0000000000..b074c5cbb8
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx.preview
@@ -0,0 +1,12 @@
+
+ Subscribe
+
+
+ Subscribe
+ Are you sure you want to subscribe?
+
+ Yes
+ No
+
+
+
\ No newline at end of file
diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.js b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.js
new file mode 100644
index 0000000000..c051ff4a3d
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.js
@@ -0,0 +1,72 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+ Are you sure you want to subscribe?
+
+ Yes
+ No
+
+
+
+ );
+}
+
+function TriggerButton(props) {
+ const className = `
+ bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900
+ py-2 px-4 rounded min-w-[80px] border-none font-sans
+ hover:bg-slate-700 dark:hover:bg-slate-200`;
+
+ return ;
+}
+
+function Popup(props) {
+ const className = `
+ bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700
+ min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100]
+ -translate-x-2/4 -translate-y-2/4 p-4`;
+
+ return ;
+}
+
+function Controls(props) {
+ return (
+
+ );
+}
+
+function CloseButton(props) {
+ const className = `
+ bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300
+ text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px]
+ hover:bg-slate-200 dark:hover:bg-slate-700`;
+
+ return ;
+}
+
+function Title(props) {
+ return ;
+}
+
+function Description(props) {
+ return ;
+}
+
+function Backdrop(props) {
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx
new file mode 100644
index 0000000000..07d9cddfbf
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx
@@ -0,0 +1,72 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+ Are you sure you want to subscribe?
+
+ Yes
+ No
+
+
+
+ );
+}
+
+function TriggerButton(props: AlertDialog.TriggerProps) {
+ const className = `
+ bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900
+ py-2 px-4 rounded min-w-[80px] border-none font-sans
+ hover:bg-slate-700 dark:hover:bg-slate-200`;
+
+ return ;
+}
+
+function Popup(props: AlertDialog.PopupProps) {
+ const className = `
+ bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700
+ min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100]
+ -translate-x-2/4 -translate-y-2/4 p-4`;
+
+ return ;
+}
+
+function Controls(props: React.ComponentPropsWithoutRef<'div'>) {
+ return (
+
+ );
+}
+
+function CloseButton(props: AlertDialog.CloseProps) {
+ const className = `
+ bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300
+ text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px]
+ hover:bg-slate-200 dark:hover:bg-slate-700`;
+
+ return ;
+}
+
+function Title(props: AlertDialog.TitleProps) {
+ return ;
+}
+
+function Description(props: AlertDialog.DescriptionProps) {
+ return ;
+}
+
+function Backdrop(props: AlertDialog.BackdropProps) {
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx.preview b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx.preview
new file mode 100644
index 0000000000..b074c5cbb8
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx.preview
@@ -0,0 +1,12 @@
+
+ Subscribe
+
+
+ Subscribe
+ Are you sure you want to subscribe?
+
+ Yes
+ No
+
+
+
\ No newline at end of file
diff --git a/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.js b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.js
new file mode 100644
index 0000000000..7e8e7fd592
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.js
@@ -0,0 +1,135 @@
+import * as React from 'react';
+import * as BaseAlertDialog from '@base_ui/react/AlertDialog';
+import { styled } from '@mui/system';
+
+export default function AlertDialogWithTransitions() {
+ return (
+
+ Open
+
+ Animated alert dialog
+
+ This alert dialog uses CSS transitions on entry and exit.
+
+
+ Close
+
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseAlertDialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transition-property: opacity, transform;
+ transition-duration: 150ms;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+
+ &[data-state='open'] {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+`,
+);
+
+const Backdrop = styled(BaseAlertDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Title = styled(BaseAlertDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseAlertDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Close = styled(BaseAlertDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx
new file mode 100644
index 0000000000..7e8e7fd592
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx
@@ -0,0 +1,135 @@
+import * as React from 'react';
+import * as BaseAlertDialog from '@base_ui/react/AlertDialog';
+import { styled } from '@mui/system';
+
+export default function AlertDialogWithTransitions() {
+ return (
+
+ Open
+
+ Animated alert dialog
+
+ This alert dialog uses CSS transitions on entry and exit.
+
+
+ Close
+
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseAlertDialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transition-property: opacity, transform;
+ transition-duration: 150ms;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+
+ &[data-state='open'] {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+`,
+);
+
+const Backdrop = styled(BaseAlertDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Title = styled(BaseAlertDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseAlertDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Close = styled(BaseAlertDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx.preview b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx.preview
new file mode 100644
index 0000000000..75f8ff39c9
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx.preview
@@ -0,0 +1,13 @@
+
+ Open
+
+ Animated alert dialog
+
+ This alert dialog uses CSS transitions on entry and exit.
+
+
+ Close
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/base/components/alert-dialog/NestedAlertDialogs.js b/docs/data/base/components/alert-dialog/NestedAlertDialogs.js
new file mode 100644
index 0000000000..5c1385314c
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/NestedAlertDialogs.js
@@ -0,0 +1,163 @@
+import * as React from 'react';
+import * as BaseAlertDialog from '@base_ui/react/AlertDialog';
+import { styled } from '@mui/system';
+
+export default function NestedAlertDialogs() {
+ return (
+
+ Open
+
+
+ Alert Dialog 1
+
+
+ Open Nested
+
+
+ Alert Dialog 2
+
+
+ Open Nested
+
+
+ Alert Dialog 3
+
+ Close
+
+
+
+ Close
+
+
+
+ Close
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseAlertDialog.Popup)(
+ ({ theme }) => `
+ --transition-duration: 150ms;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ visibility: hidden;
+ opacity: 0.5;
+ transition:
+ transform var(--transition-duration) ease-in,
+ opacity var(--transition-duration) ease-in,
+ visibility var(--transition-duration) step-end;
+
+ &[data-state='open'] {
+ @starting-style {
+ & {
+ transform: translate(-50%, -35%) scale(0.8) translateY(0);
+ opacity: 0.5;
+ }
+ }
+
+ visibility: visible;
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ transition:
+ transform var(--transition-duration) ease-out,
+ opacity var(--transition-duration) ease-out,
+ visibility var(--transition-duration) step-start;
+ }
+`,
+);
+
+const Title = styled(BaseAlertDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseAlertDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Backdrop = styled(BaseAlertDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Close = styled(BaseAlertDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/alert-dialog/NestedAlertDialogs.tsx b/docs/data/base/components/alert-dialog/NestedAlertDialogs.tsx
new file mode 100644
index 0000000000..5c1385314c
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/NestedAlertDialogs.tsx
@@ -0,0 +1,163 @@
+import * as React from 'react';
+import * as BaseAlertDialog from '@base_ui/react/AlertDialog';
+import { styled } from '@mui/system';
+
+export default function NestedAlertDialogs() {
+ return (
+
+ Open
+
+
+ Alert Dialog 1
+
+
+ Open Nested
+
+
+ Alert Dialog 2
+
+
+ Open Nested
+
+
+ Alert Dialog 3
+
+ Close
+
+
+
+ Close
+
+
+
+ Close
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseAlertDialog.Popup)(
+ ({ theme }) => `
+ --transition-duration: 150ms;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ visibility: hidden;
+ opacity: 0.5;
+ transition:
+ transform var(--transition-duration) ease-in,
+ opacity var(--transition-duration) ease-in,
+ visibility var(--transition-duration) step-end;
+
+ &[data-state='open'] {
+ @starting-style {
+ & {
+ transform: translate(-50%, -35%) scale(0.8) translateY(0);
+ opacity: 0.5;
+ }
+ }
+
+ visibility: visible;
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ transition:
+ transform var(--transition-duration) ease-out,
+ opacity var(--transition-duration) ease-out,
+ visibility var(--transition-duration) step-start;
+ }
+`,
+);
+
+const Title = styled(BaseAlertDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseAlertDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Backdrop = styled(BaseAlertDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Close = styled(BaseAlertDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/alert-dialog/alert-dialog.md b/docs/data/base/components/alert-dialog/alert-dialog.md
new file mode 100644
index 0000000000..ad67481d4b
--- /dev/null
+++ b/docs/data/base/components/alert-dialog/alert-dialog.md
@@ -0,0 +1,298 @@
+---
+productId: base-ui
+title: React Alert Dialog component
+components: AlertDialogBackdrop, AlertDialogClose, AlertDialogDescription, AlertDialogPopup, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger
+githubLabel: 'component: alert-dialog'
+waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
+---
+
+# Alert Dialog
+
+Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.
+
+{{"component": "@mui/docs/ComponentLinkHeader", "design": false}}
+
+{{"component": "modules/components/ComponentPageTabs.js"}}
+
+{{"demo": "AlertDialogIntroduction", "defaultCodeOpen": false, "bg": "gradient"}}
+
+## Installation
+
+Base UI components are all available as a single package.
+
+
+
+```bash npm
+npm install @base_ui/react
+```
+
+```bash yarn
+yarn add @base_ui/react
+```
+
+```bash pnpm
+pnpm add @base_ui/react
+```
+
+
+
+Once you have the package installed, import the component.
+
+```ts
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+```
+
+## Anatomy
+
+Alert Dialogs are implemented using a collection of related components:
+
+- ` ` is a top-level component that facilitates communication between other components. It does not render to the DOM.
+- ` ` is the alert dialog panel itself.
+- ` ` is the background element appearing when a popup is visible. Use it to indicate that the page is inert. The Backdrop must be a sibling of the Popup component.
+- ` ` is the component (a button by default) that, when clicked, shows the popup. When it's not provided, the visibility of the Alert Dialog can be controlled with its `open` prop (see [Controlled vs. uncontrolled behavior](#controlled-vs-uncontrolled-behavior)).
+- ` ` renders a button that closes the popup. You can attach your own click handlers to it to perform additional actions.
+- ` ` is an header element displaying the title of the alert dialog. It is referenced in the Dialog's ARIA attributes to properly announce it.
+- ` ` is an element describing of the dialog. It is referenced in the Dialog's ARIA attributes to properly announce it.
+
+```tsx
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Alert dialogs vs. dialogs
+
+The Alert Dialog is in many ways similar to the [Dialog](/base-ui/react-dialog/) component.
+Alert dialogs should be used in cases where the normal user's workflow needs to be interrupted to get a response.
+Therefore alert dialogs are always modal and cannot be dismissed any other way than by pressing a button inside them.
+
+## Controlled vs. uncontrolled behavior
+
+The simplest way to control the visibility of the alert dialog is to use the `AlertDialog.Trigger` and `AlertDialog.Close` components.
+
+You can set the initial state with the `defaultOpen` prop.
+
+```tsx
+
+ Open
+
+ Demo dialog
+ Close
+
+
+```
+
+Doing so ensures that the accessibity attributes are set correctly so that the trigger button is approriately announced by assistive technologies.
+
+If you need to control the visibility programmatically from the outside, use the `value` prop.
+You can still use the `AlertDialog.Trigger` and `AlertDialog.Close` components (though it's not necessary), but you need to make sure to create a handler for the `onOpenChange` event and update the state manually.
+
+```tsx
+const [open, setOpen] = React.useState(false);
+
+return (
+
+ Open
+
+ Demo dialog
+ Close
+
+
+);
+```
+
+## Nested dialogs
+
+An alert dialog can open another dialog (or alert dialog).
+At times, it may be useful to know how may open sub-dialogs a given alert dialog has.
+One example of this could be styling the bottom dialog in a way they appear below the top-most one.
+
+The number of open child dialogs is present in the `data-nested-dialogs` attribute and in the `--nested-dialogs` CSS variable on the `` component.
+
+{{"demo": "NestedAlertDialogs.js"}}
+
+Note that when dialogs are nested, only the bottom-most backdrop is rendered.
+
+## Animation
+
+The `` and `` components support transitions on entry and exit.
+
+CSS animations and transitions are supported out of the box.
+If a component has a transition or animation applied to it when it closes, it will be unmounted only after the animation finishes.
+
+As this detection of exit animations requires an extra render, you may opt out of it by setting the `animated` prop on Popup and Backdrop to `false`.
+We also recommend doing so in automated tests, to avoid asynchronous behavior and make testing easier.
+
+Alternatively, you can use JS-based animations with a library like framer-motion, React Spring, or similar.
+With this approach set the `keepMounted` to `true` and let the animation library control mounting and unmounting.
+
+### CSS transitions
+
+Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior:
+
+```jsx
+Alert
+```
+
+```css
+.AlertDialogPopup {
+ transition-property: opacity, transform;
+ transition-duration: 0.2s;
+ /* Represents the final styles once exited */
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+}
+
+/* Represents the final styles once entered */
+.AlertDialogPopup[data-state='open'] {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+}
+
+/* Represents the initial styles when entering */
+.AlertDialogPopup[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+}
+```
+
+Styles need to be applied in three states:
+
+- The exiting styles, placed on the base element class
+- The open styles, placed on the base element class with `[data-state="open"]`
+- The entering styles, placed on the base element class with `[data-entering]`
+
+{{"demo": "AlertDialogWithTransitions.js"}}
+
+In newer browsers, there is a feature called `@starting-style` which allows transitions to occur on open for conditionally-mounted components:
+
+```css
+/* Base UI API - Polyfill */
+.AlertDialogPopup[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+}
+
+/* Official Browser API - no Firefox support as of May 2024 */
+@starting-style {
+ .AlertDialogPopup[data-state='open'] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+}
+```
+
+### CSS animations
+
+CSS animations can also be used, requiring only two separate declarations:
+
+```css
+@keyframes scale-in {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+}
+
+@keyframes scale-out {
+ to {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+}
+
+.AlertDialogPopup {
+ animation: scale-in 0.2s forwards;
+}
+
+.AlertDialogPopup[data-exiting] {
+ animation: scale-out 0.2s forwards;
+}
+```
+
+### JavaScript animations
+
+The `keepMounted` prop lets an external library control the mounting, for example `framer-motion`'s `AnimatePresence` component.
+
+```js
+function App() {
+ const [open, setOpen] = useState(false);
+ return (
+
+ Trigger
+
+ {open && (
+
+ }
+ >
+ Alert Dialog
+
+ )}
+
+
+ );
+}
+```
+
+### Animation states
+
+Four states are available as data attributes to animate the dialog, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the `keepMounted` prop.
+
+- `[data-state="open"]` - `open` state is `true`.
+- `[data-state="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing.
+- `[data-entering]` - the popup was just inserted to the DOM. The attribute is removed 1 animation frame later. Enables "starting styles" upon insertion for conditional rendering.
+- `[data-exiting]` - the popup is in the process of being removed from the DOM, but is still mounted.
+
+## Composing a custom React component
+
+Use the `render` prop to override the rendered element:
+
+```jsx
+ } />
+// or
+ } />
+```
+
+## Accessibility
+
+Using the `` sets the required accessibility attributes on the trigger button.
+If you prefer controlling the open state differently, you need to apply these attributes on your own:
+
+```tsx
+const [open, setOpen] = React.useState(false);
+
+return (
+
+
setOpen(true)}
+ >
+ Open
+
+
+
+
+
+
+);
+```
diff --git a/docs/data/base/components/dialog/DialogWithTransitions.js b/docs/data/base/components/dialog/DialogWithTransitions.js
new file mode 100644
index 0000000000..901f682060
--- /dev/null
+++ b/docs/data/base/components/dialog/DialogWithTransitions.js
@@ -0,0 +1,135 @@
+import * as React from 'react';
+import * as BaseDialog from '@base_ui/react/Dialog';
+import { styled } from '@mui/system';
+
+export default function DialogWithTransitions() {
+ return (
+
+ Open
+
+ Animated dialog
+
+ This dialog uses CSS transitions on entry and exit.
+
+
+ Close
+
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseDialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transition-property: opacity, transform;
+ transition-duration: 150ms;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+
+ &[data-state='open'] {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+`,
+);
+
+const Backdrop = styled(BaseDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Title = styled(BaseDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Close = styled(BaseDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/dialog/DialogWithTransitions.tsx b/docs/data/base/components/dialog/DialogWithTransitions.tsx
new file mode 100644
index 0000000000..901f682060
--- /dev/null
+++ b/docs/data/base/components/dialog/DialogWithTransitions.tsx
@@ -0,0 +1,135 @@
+import * as React from 'react';
+import * as BaseDialog from '@base_ui/react/Dialog';
+import { styled } from '@mui/system';
+
+export default function DialogWithTransitions() {
+ return (
+
+ Open
+
+ Animated dialog
+
+ This dialog uses CSS transitions on entry and exit.
+
+
+ Close
+
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseDialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transition-property: opacity, transform;
+ transition-duration: 150ms;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+
+ &[data-state='open'] {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+`,
+);
+
+const Backdrop = styled(BaseDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Title = styled(BaseDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Close = styled(BaseDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/dialog/DialogWithTransitions.tsx.preview b/docs/data/base/components/dialog/DialogWithTransitions.tsx.preview
new file mode 100644
index 0000000000..15e757d86a
--- /dev/null
+++ b/docs/data/base/components/dialog/DialogWithTransitions.tsx.preview
@@ -0,0 +1,13 @@
+
+ Open
+
+ Animated dialog
+
+ This dialog uses CSS transitions on entry and exit.
+
+
+ Close
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/base/components/dialog/NestedDialogs.js b/docs/data/base/components/dialog/NestedDialogs.js
new file mode 100644
index 0000000000..3a42cef5d3
--- /dev/null
+++ b/docs/data/base/components/dialog/NestedDialogs.js
@@ -0,0 +1,163 @@
+import * as React from 'react';
+import * as BaseDialog from '@base_ui/react/Dialog';
+import { styled } from '@mui/system';
+
+export default function NestedDialogs() {
+ return (
+
+ Open
+
+
+ Dialog 1
+
+
+ Open Nested
+
+
+ Dialog 2
+
+
+ Open Nested
+
+
+ Dialog 3
+
+ Close
+
+
+
+ Close
+
+
+
+ Close
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseDialog.Popup)(
+ ({ theme }) => `
+ --transition-duration: 150ms;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ visibility: hidden;
+ opacity: 0.5;
+ transition:
+ transform var(--transition-duration) ease-in,
+ opacity var(--transition-duration) ease-in,
+ visibility var(--transition-duration) step-end;
+
+ &[data-state='open'] {
+ @starting-style {
+ & {
+ transform: translate(-50%, -35%) scale(0.8) translateY(0);
+ opacity: 0.5;
+ }
+ }
+
+ visibility: visible;
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ transition:
+ transform var(--transition-duration) ease-out,
+ opacity var(--transition-duration) ease-out,
+ visibility var(--transition-duration) step-start;
+ }
+`,
+);
+
+const Title = styled(BaseDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Close = styled(BaseDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Backdrop = styled(BaseDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/dialog/NestedDialogs.tsx b/docs/data/base/components/dialog/NestedDialogs.tsx
new file mode 100644
index 0000000000..3a42cef5d3
--- /dev/null
+++ b/docs/data/base/components/dialog/NestedDialogs.tsx
@@ -0,0 +1,163 @@
+import * as React from 'react';
+import * as BaseDialog from '@base_ui/react/Dialog';
+import { styled } from '@mui/system';
+
+export default function NestedDialogs() {
+ return (
+
+ Open
+
+
+ Dialog 1
+
+
+ Open Nested
+
+
+ Dialog 2
+
+
+ Open Nested
+
+
+ Dialog 3
+
+ Close
+
+
+
+ Close
+
+
+
+ Close
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const Popup = styled(BaseDialog.Popup)(
+ ({ theme }) => `
+ --transition-duration: 150ms;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: IBM Plex Sans;
+ padding: 16px;
+ z-index: 2100;
+ transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ visibility: hidden;
+ opacity: 0.5;
+ transition:
+ transform var(--transition-duration) ease-in,
+ opacity var(--transition-duration) ease-in,
+ visibility var(--transition-duration) step-end;
+
+ &[data-state='open'] {
+ @starting-style {
+ & {
+ transform: translate(-50%, -35%) scale(0.8) translateY(0);
+ opacity: 0.5;
+ }
+ }
+
+ visibility: visible;
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ transition:
+ transform var(--transition-duration) ease-out,
+ opacity var(--transition-duration) ease-out,
+ visibility var(--transition-duration) step-start;
+ }
+`,
+);
+
+const Title = styled(BaseDialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Trigger = styled(BaseDialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family:
+ "IBM Plex Sans",
+ sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Close = styled(BaseDialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: IBM Plex Sans, sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Backdrop = styled(BaseDialog.Backdrop)`
+ background-color: rgb(0 0 0 / 0.2);
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ backdrop-filter: blur(0);
+ opacity: 0;
+ transition-property: opacity, backdrop-filter;
+ transition-duration: 250ms;
+ transition-timing-function: ease-in;
+
+ &[data-state='open'] {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ transition-timing-function: ease-out;
+ }
+
+ &[data-entering] {
+ backdrop-filter: blur(0);
+ opacity: 0;
+ }
+`;
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.js b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.js
new file mode 100644
index 0000000000..f50a332498
--- /dev/null
+++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.js
@@ -0,0 +1,133 @@
+import * as React from 'react';
+import * as Dialog from '@base_ui/react/Dialog';
+import { useTheme } from '@mui/system';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+
+ Subscribe
+
+
+ Subscribe
+
+ Enter your email address to subscribe to our newsletter.
+
+
+
+ Subscribe
+ Cancel
+
+
+
+
+
+ );
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+function Styles() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.tsx b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.tsx
new file mode 100644
index 0000000000..f50a332498
--- /dev/null
+++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.tsx
@@ -0,0 +1,133 @@
+import * as React from 'react';
+import * as Dialog from '@base_ui/react/Dialog';
+import { useTheme } from '@mui/system';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+
+ Subscribe
+
+
+ Subscribe
+
+ Enter your email address to subscribe to our newsletter.
+
+
+
+ Subscribe
+ Cancel
+
+
+
+
+
+ );
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+function Styles() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.js b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.js
new file mode 100644
index 0000000000..f52c7b9977
--- /dev/null
+++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.js
@@ -0,0 +1,121 @@
+import * as React from 'react';
+import * as Dialog from '@base_ui/react/Dialog';
+import { styled } from '@mui/system';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+
+ Enter your email address to subscribe to our newsletter.
+
+
+
+ Subscribe
+ Cancel
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const TriggerButton = styled(Dialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family: "IBM Plex Sans", sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Popup = styled(Dialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: "IBM Plex Sans", sans-serif;
+ transform: translate(-50%, -50%);
+ padding: 16px;
+ z-index: 2100;
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
+
+const CloseButton = styled(Dialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: "IBM Plex Sans", sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Title = styled(Dialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Description = styled(Dialog.Description)``;
+
+const Backdrop = styled(Dialog.Backdrop)`
+ background: rgb(0 0 0 / 0.35);
+ position: fixed;
+ inset: 0;
+ backdrop-filter: blur(4px);
+ z-index: 2000;
+`;
+
+const TextField = styled('input')`
+ padding: 8px;
+ border-radius: 4px;
+ border: 1px solid ${grey[300]};
+ font-family: 'IBM Plex Sans', sans-serif;
+ margin: 16px 0;
+ width: 100%;
+ box-sizing: border-box;
+`;
diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.tsx b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.tsx
new file mode 100644
index 0000000000..f52c7b9977
--- /dev/null
+++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.tsx
@@ -0,0 +1,121 @@
+import * as React from 'react';
+import * as Dialog from '@base_ui/react/Dialog';
+import { styled } from '@mui/system';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+
+ Enter your email address to subscribe to our newsletter.
+
+
+
+ Subscribe
+ Cancel
+
+
+
+ );
+}
+
+const grey = {
+ 900: '#0f172a',
+ 800: '#1e293b',
+ 700: '#334155',
+ 500: '#64748b',
+ 300: '#cbd5e1',
+ 200: '#e2e8f0',
+ 100: '#f1f5f9',
+ 50: '#f8fafc',
+};
+
+const TriggerButton = styled(Dialog.Trigger)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ border: none;
+ font-family: "IBM Plex Sans", sans-serif;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]};
+ }
+`,
+);
+
+const Popup = styled(Dialog.Popup)(
+ ({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]};
+ min-width: 400px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ font-family: "IBM Plex Sans", sans-serif;
+ transform: translate(-50%, -50%);
+ padding: 16px;
+ z-index: 2100;
+`,
+);
+
+const Controls = styled('div')(
+ ({ theme }) => `
+ display: flex;
+ flex-direction: row-reverse;
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]};
+ gap: 8px;
+ padding: 16px;
+ margin: 32px -16px -16px;
+`,
+);
+
+const CloseButton = styled(Dialog.Close)(
+ ({ theme }) => `
+ background-color: transparent;
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]};
+ color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]};
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-family: "IBM Plex Sans", sans-serif;
+ min-width: 80px;
+
+ &:hover {
+ background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ }
+`,
+);
+
+const Title = styled(Dialog.Title)`
+ font-size: 1.25rem;
+`;
+
+const Description = styled(Dialog.Description)``;
+
+const Backdrop = styled(Dialog.Backdrop)`
+ background: rgb(0 0 0 / 0.35);
+ position: fixed;
+ inset: 0;
+ backdrop-filter: blur(4px);
+ z-index: 2000;
+`;
+
+const TextField = styled('input')`
+ padding: 8px;
+ border-radius: 4px;
+ border: 1px solid ${grey[300]};
+ font-family: 'IBM Plex Sans', sans-serif;
+ margin: 16px 0;
+ width: 100%;
+ box-sizing: border-box;
+`;
diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.js b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.js
new file mode 100644
index 0000000000..ddcd4a4dc3
--- /dev/null
+++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.js
@@ -0,0 +1,87 @@
+import * as React from 'react';
+import * as Dialog from '@base_ui/react/Dialog';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+
+ Enter your email address to subscribe to our newsletter.
+
+
+
+ Subscribe
+ Cancel
+
+
+
+ );
+}
+
+function TriggerButton(props) {
+ const className = `
+ bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900
+ py-2 px-4 rounded min-w-[80px] border-none font-sans
+ hover:bg-slate-700 dark:hover:bg-slate-200`;
+
+ return ;
+}
+
+function Popup(props) {
+ const className = `
+ bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700
+ min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100]
+ -translate-x-2/4 -translate-y-2/4 p-4`;
+
+ return ;
+}
+
+function Controls(props) {
+ return (
+
+ );
+}
+
+function CloseButton(props) {
+ const className = `
+ bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300
+ text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px]
+ hover:bg-slate-200 dark:hover:bg-slate-700`;
+
+ return ;
+}
+
+function Title(props) {
+ return ;
+}
+
+function Description(props) {
+ return ;
+}
+
+function Backdrop(props) {
+ return (
+
+ );
+}
+
+function TextField(props) {
+ const className = `
+ w-full p-2 mt-4 font-sans
+ border-[1px] border-solid border-slate-300 dark:border-slate-700 rounded
+ `;
+ return ;
+}
diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.tsx b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.tsx
new file mode 100644
index 0000000000..ecae78b79e
--- /dev/null
+++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.tsx
@@ -0,0 +1,87 @@
+import * as React from 'react';
+import * as Dialog from '@base_ui/react/Dialog';
+
+export default function UnstyledDialogIntroduction() {
+ return (
+
+ Subscribe
+
+
+ Subscribe
+
+ Enter your email address to subscribe to our newsletter.
+
+
+
+ Subscribe
+ Cancel
+
+
+
+ );
+}
+
+function TriggerButton(props: Dialog.TriggerProps) {
+ const className = `
+ bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900
+ py-2 px-4 rounded min-w-[80px] border-none font-sans
+ hover:bg-slate-700 dark:hover:bg-slate-200`;
+
+ return ;
+}
+
+function Popup(props: Dialog.PopupProps) {
+ const className = `
+ bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700
+ min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100]
+ -translate-x-2/4 -translate-y-2/4 p-4`;
+
+ return ;
+}
+
+function Controls(props: React.ComponentPropsWithoutRef<'div'>) {
+ return (
+
+ );
+}
+
+function CloseButton(props: Dialog.CloseProps) {
+ const className = `
+ bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300
+ text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px]
+ hover:bg-slate-200 dark:hover:bg-slate-700`;
+
+ return ;
+}
+
+function Title(props: Dialog.TitleProps) {
+ return ;
+}
+
+function Description(props: Dialog.DescriptionProps) {
+ return ;
+}
+
+function Backdrop(props: Dialog.BackdropProps) {
+ return (
+
+ );
+}
+
+function TextField(props: React.ComponentPropsWithoutRef<'input'>) {
+ const className = `
+ w-full p-2 mt-4 font-sans
+ border-[1px] border-solid border-slate-300 dark:border-slate-700 rounded
+ `;
+ return ;
+}
diff --git a/docs/data/base/components/dialog/dialog.md b/docs/data/base/components/dialog/dialog.md
new file mode 100644
index 0000000000..ac4f7c7e66
--- /dev/null
+++ b/docs/data/base/components/dialog/dialog.md
@@ -0,0 +1,320 @@
+---
+productId: base-ui
+title: React Dialog component and hook
+components: DialogBackdrop, DialogClose, DialogDescription, DialogPopup, DialogRoot, DialogTitle, DialogTrigger
+hooks: useDialogClose, useDialogPopup, useDialogRoot, useDialogTrigger
+githubLabel: 'component: dialog'
+waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
+---
+
+# Dialog
+
+Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.
+
+{{"component": "@mui/docs/ComponentLinkHeader", "design": false}}
+
+{{"component": "modules/components/ComponentPageTabs.js"}}
+
+{{"demo": "UnstyledDialogIntroduction", "defaultCodeOpen": false, "bg": "gradient"}}
+
+## Installation
+
+Base UI components are all available as a single package.
+
+
+
+```bash npm
+npm install @base_ui/react
+```
+
+```bash yarn
+yarn add @base_ui/react
+```
+
+```bash pnpm
+pnpm add @base_ui/react
+```
+
+
+
+Once you have the package installed, import the component.
+
+```ts
+import * as Dialog from '@base_ui/react/Dialog';
+```
+
+## Anatomy
+
+Dialogs are implemented using a collection of related components:
+
+- ` ` is a top-level component that facilitates communication between other components. It does not render to the DOM.
+- ` ` is the dialog panel itself.
+- ` ` is the background element appearing when a popup is visible. Use it to indicate that the page is inert when using a modal dialog. The Backdrop must be a sibling of the Popup component. It is mandatory for modal dialogs.
+- ` ` is the component (a button by default) that, when clicked, shows the popup. When it's not provided, the visibility of the Dialog can be controlled with its `open` prop (see [Controlled vs. uncontrolled behavior](#controlled-vs-uncontrolled-behavior)).
+- ` ` renders a button that closes the popup. You can attach your own click handlers to it to perform additional actions.
+- ` ` is an header element displaying the title of the dialog. It is referenced in the Dialog's ARIA attributes to properly announce the dialog.
+- ` ` is an element describing of the dialog. It is referenced in the Dialog's ARIA attributes to properly announce the dialog.
+
+```tsx
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Modal and non-modal dialogs
+
+Dialogs can be either modal (rendering the rest of the page inert) or non-modal.
+A non-modal dialog can be used to implement tool windows.
+
+The `modal` prop of the `` controls this.
+By default Dialogs are modal.
+
+```tsx
+{/* ... */}
+```
+
+:::warning
+To make the Dialog fully modal, you must have a Backdrop component and style it so it covers the entire viewport, blocking pointer interaction with other elements on the page.
+:::
+
+## Closing the dialog
+
+The default way to close the dialog is clicking on the `` component.
+Dialogs also close when the user clicks outside of them or presses the Esc key.
+
+Closing on outside click can be disabled with a `dismissible` prop on the `Dialog.Root`:
+
+```tsx
+{/* ... */}
+```
+
+## Controlled vs. uncontrolled behavior
+
+The simplest way to control the visibility of the dialog is to use the `` and `` components.
+
+You can set the initial state with the `defaultOpen` prop.
+
+```tsx
+
+ Open
+
+ Demo dialog
+ Close
+
+
+```
+
+Doing so ensures that the accessibity attributes are set correctly so that the trigger button is approriately announced by assistive technologies.
+
+If you need to control the visibility programmatically from the outside, use the `value` prop.
+You can still use the `` and `` components (though it's not necessary), but you need to make sure to create a handler for the `onOpenChange` event and update the state manually.
+
+```tsx
+const [open, setOpen] = React.useState(false);
+
+return (
+
+ Open
+
+ Demo dialog
+ Close
+
+
+);
+```
+
+## Nested dialogs
+
+A dialog can open another dialog.
+At times, it may be useful to know how may open sub-dialogs a given dialog has.
+One example of this could be styling the bottom dialog in a way they appear below the top-most one.
+
+The number of open child dialogs is present in the `data-nested-dialogs` attribute and in the `--nested-dialogs` CSS variable on the `` component.
+
+{{"demo": "NestedDialogs.js"}}
+
+Note that when dialogs are nested, only the bottom-most backdrop is rendered.
+
+## Animation
+
+The `` and `` components support transitions on entry and exit.
+
+CSS animations and transitions are supported out of the box.
+If a component has a transition or animation applied to it when it closes, it will be unmounted only after the animation finishes.
+
+As this detection of exit animations requires an extra render, you may opt out of it by setting the `animated` prop on Root to `false`.
+We also recommend doing so in automated tests, to avoid asynchronous behavior and make testing easier.
+
+Alternatively, you can use JS-based animations with a library like framer-motion, React Spring, or similar.
+With this approach set the `keepMounted` to `true` and let the animation library control mounting and unmounting.
+
+### CSS transitions
+
+Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior:
+
+```jsx
+Dialog
+```
+
+```css
+.DialogPopup {
+ transition-property: opacity, transform;
+ transition-duration: 0.2s;
+ /* Represents the final styles once exited */
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+}
+
+/* Represents the final styles once entered */
+.DialogPopup[data-state='open'] {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+}
+
+/* Represents the initial styles when entering */
+.DialogPopup[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+}
+```
+
+Styles need to be applied in three states:
+
+- The exiting styles, placed on the base element class
+- The open styles, placed on the base element class with `[data-state="open"]`
+- The entering styles, placed on the base element class with `[data-entering]`
+
+{{"demo": "DialogWithTransitions.js"}}
+
+In newer browsers, there is a feature called `@starting-style` which allows transitions to occur on open for conditionally-mounted components:
+
+```css
+/* Base UI API - Polyfill */
+.DialogPopup[data-entering] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+}
+
+/* Official Browser API - no Firefox support as of May 2024 */
+@starting-style {
+ .DialogPopup[data-state='open'] {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+}
+```
+
+### CSS animations
+
+CSS animations can also be used, requiring only two separate declarations:
+
+```css
+@keyframes scale-in {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+}
+
+@keyframes scale-out {
+ to {
+ opacity: 0;
+ transform: translate(-50%, -35%) scale(0.8);
+ }
+}
+
+.DialogPopup {
+ animation: scale-in 0.2s forwards;
+}
+
+.DialogPopup[data-exiting] {
+ animation: scale-out 0.2s forwards;
+}
+```
+
+### JavaScript animations
+
+The `keepMounted` prop lets an external library control the mounting, for example `framer-motion`'s `AnimatePresence` component.
+
+```js
+function App() {
+ const [open, setOpen] = useState(false);
+ return (
+
+ Trigger
+
+ {open && (
+
+ }
+ >
+ Dialog
+
+ )}
+
+
+ );
+}
+```
+
+### Animation states
+
+Four states are available as data attributes to animate the dialog, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the `keepMounted` prop.
+
+- `[data-state="open"]` - `open` state is `true`.
+- `[data-state="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing.
+- `[data-entering]` - the popup was just inserted to the DOM. The attribute is removed 1 animation frame later. Enables "starting styles" upon insertion for conditional rendering.
+- `[data-exiting]` - the popup is in the process of being removed from the DOM, but is still mounted.
+
+## Composing a custom React component
+
+Use the `render` prop to override the rendered element:
+
+```jsx
+ } />
+// or
+ } />
+```
+
+## Accessibility
+
+Using the `` sets the required accessibility attributes on the trigger button.
+If you prefer controlling the open state differently, you need to apply these attributes on your own:
+
+```tsx
+const [open, setOpen] = React.useState(false);
+
+return (
+
+ setOpen(true)}
+ >
+ Open
+
+
+
+
+
+
+);
+```
diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts
index 9f049a2afa..38036691a8 100644
--- a/docs/data/base/pages.ts
+++ b/docs/data/base/pages.ts
@@ -43,13 +43,15 @@ const pages: readonly MuiPage[] = [
{ pathname: '/base-ui/react-tooltip', title: 'Tooltip' },
],
},
- // {
- // pathname: '/base-ui/components/feedback',
- // subheader: 'feedback',
- // children: [
- // { pathname: '/base-ui/react-snackbar', title: 'Snackbar' },
- // ],
- // },
+ {
+ pathname: '/base-ui/components/feedback',
+ subheader: 'feedback',
+ children: [
+ { pathname: '/base-ui/react-alert-dialog', title: 'Alert Dialog' },
+ { pathname: '/base-ui/react-dialog', title: 'Dialog' },
+ // { pathname: '/base-ui/react-snackbar', title: 'Snackbar' },
+ ],
+ },
// {
// pathname: '/base-ui/components/surfaces',
// subheader: 'surfaces',
diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js
index 17e5e1f9eb..1f00bed512 100644
--- a/docs/data/base/pagesApi.js
+++ b/docs/data/base/pagesApi.js
@@ -1,4 +1,32 @@
module.exports = [
+ {
+ pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-backdrop',
+ title: 'AlertDialogBackdrop',
+ },
+ {
+ pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-close',
+ title: 'AlertDialogClose',
+ },
+ {
+ pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-description',
+ title: 'AlertDialogDescription',
+ },
+ {
+ pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-popup',
+ title: 'AlertDialogPopup',
+ },
+ {
+ pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-root',
+ title: 'AlertDialogRoot',
+ },
+ {
+ pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-title',
+ title: 'AlertDialogTitle',
+ },
+ {
+ pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-trigger',
+ title: 'AlertDialogTrigger',
+ },
{ pathname: '/base-ui/react-badge/components-api/#badge', title: 'Badge' },
{ pathname: '/base-ui/react-button/components-api/#button', title: 'Button' },
{
@@ -22,6 +50,34 @@ module.exports = [
pathname: '/base-ui/react-transitions/components-api/#css-transition',
title: 'CssTransition',
},
+ {
+ pathname: '/base-ui/react-dialog/components-api/#dialog-backdrop',
+ title: 'DialogBackdrop',
+ },
+ {
+ pathname: '/base-ui/react-dialog/components-api/#dialog-close',
+ title: 'DialogClose',
+ },
+ {
+ pathname: '/base-ui/react-dialog/components-api/#dialog-description',
+ title: 'DialogDescription',
+ },
+ {
+ pathname: '/base-ui/react-dialog/components-api/#dialog-popup',
+ title: 'DialogPopup',
+ },
+ {
+ pathname: '/base-ui/react-dialog/components-api/#dialog-root',
+ title: 'DialogRoot',
+ },
+ {
+ pathname: '/base-ui/react-dialog/components-api/#dialog-title',
+ title: 'DialogTitle',
+ },
+ {
+ pathname: '/base-ui/react-dialog/components-api/#dialog-trigger',
+ title: 'DialogTrigger',
+ },
{ pathname: '/base-ui/react-menu/components-api/#dropdown', title: 'Dropdown' },
{
pathname: '/base-ui/react-focus-trap/components-api/#focus-trap',
@@ -141,6 +197,22 @@ module.exports = [
pathname: '/base-ui/react-checkbox/hooks-api/#use-checkbox-root',
title: 'useCheckboxRoot',
},
+ {
+ pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-close',
+ title: 'useDialogClose',
+ },
+ {
+ pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-popup',
+ title: 'useDialogPopup',
+ },
+ {
+ pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-root',
+ title: 'useDialogRoot',
+ },
+ {
+ pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-trigger',
+ title: 'useDialogTrigger',
+ },
{ pathname: '/base-ui/react-menu/hooks-api/#use-dropdown', title: 'useDropdown' },
{
pathname: '/base-ui/react-form-control/hooks-api/#use-form-control-context',
diff --git a/docs/pages/base-ui/api/alert-dialog-backdrop.json b/docs/pages/base-ui/api/alert-dialog-backdrop.json
new file mode 100644
index 0000000000..ced30b97d5
--- /dev/null
+++ b/docs/pages/base-ui/api/alert-dialog-backdrop.json
@@ -0,0 +1,20 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "keepMounted": { "type": { "name": "bool" }, "default": "false" },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "AlertDialogBackdrop",
+ "imports": [
+ "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogBackdrop = AlertDialog.Backdrop;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "AlertDialogBackdrop",
+ "forwardsRefTo": "HTMLDivElement",
+ "filename": "/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/alert-dialog-close.json b/docs/pages/base-ui/api/alert-dialog-close.json
new file mode 100644
index 0000000000..3ae0ccd4b4
--- /dev/null
+++ b/docs/pages/base-ui/api/alert-dialog-close.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "AlertDialogClose",
+ "imports": [
+ "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogClose = AlertDialog.Close;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "AlertDialogClose",
+ "forwardsRefTo": "HTMLButtonElement",
+ "filename": "/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/alert-dialog-description.json b/docs/pages/base-ui/api/alert-dialog-description.json
new file mode 100644
index 0000000000..2875bcc0a9
--- /dev/null
+++ b/docs/pages/base-ui/api/alert-dialog-description.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "AlertDialogDescription",
+ "imports": [
+ "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogDescription = AlertDialog.Description;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "AlertDialogDescription",
+ "forwardsRefTo": "HTMLParagraphElement",
+ "filename": "/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/alert-dialog-popup.json b/docs/pages/base-ui/api/alert-dialog-popup.json
new file mode 100644
index 0000000000..ea9878c52a
--- /dev/null
+++ b/docs/pages/base-ui/api/alert-dialog-popup.json
@@ -0,0 +1,21 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "container": { "type": { "name": "union", "description": "HTML element | ref" } },
+ "keepMounted": { "type": { "name": "bool" }, "default": "false" },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "AlertDialogPopup",
+ "imports": [
+ "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogPopup = AlertDialog.Popup;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "AlertDialogPopup",
+ "forwardsRefTo": "HTMLDivElement",
+ "filename": "/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/alert-dialog-root.json b/docs/pages/base-ui/api/alert-dialog-root.json
new file mode 100644
index 0000000000..a733c1d74f
--- /dev/null
+++ b/docs/pages/base-ui/api/alert-dialog-root.json
@@ -0,0 +1,20 @@
+{
+ "props": {
+ "animated": { "type": { "name": "bool" }, "default": "true" },
+ "defaultOpen": { "type": { "name": "bool" } },
+ "onOpenChange": { "type": { "name": "func" } },
+ "open": { "type": { "name": "bool" } }
+ },
+ "name": "AlertDialogRoot",
+ "imports": [
+ "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogRoot = AlertDialog.Root;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": null,
+ "muiName": "AlertDialogRoot",
+ "filename": "/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/alert-dialog-title.json b/docs/pages/base-ui/api/alert-dialog-title.json
new file mode 100644
index 0000000000..cbea492a58
--- /dev/null
+++ b/docs/pages/base-ui/api/alert-dialog-title.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "AlertDialogTitle",
+ "imports": [
+ "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogTitle = AlertDialog.Title;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "AlertDialogTitle",
+ "forwardsRefTo": "HTMLHeadingElement",
+ "filename": "/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/alert-dialog-trigger.json b/docs/pages/base-ui/api/alert-dialog-trigger.json
new file mode 100644
index 0000000000..a2ba533cf7
--- /dev/null
+++ b/docs/pages/base-ui/api/alert-dialog-trigger.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "AlertDialogTrigger",
+ "imports": [
+ "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogTrigger = AlertDialog.Trigger;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "AlertDialogTrigger",
+ "forwardsRefTo": "HTMLButtonElement",
+ "filename": "/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/dialog-backdrop.json b/docs/pages/base-ui/api/dialog-backdrop.json
new file mode 100644
index 0000000000..1256870dac
--- /dev/null
+++ b/docs/pages/base-ui/api/dialog-backdrop.json
@@ -0,0 +1,20 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "keepMounted": { "type": { "name": "bool" }, "default": "false" },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "DialogBackdrop",
+ "imports": [
+ "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogBackdrop = Dialog.Backdrop;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "DialogBackdrop",
+ "forwardsRefTo": "HTMLDivElement",
+ "filename": "/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/dialog-close.json b/docs/pages/base-ui/api/dialog-close.json
new file mode 100644
index 0000000000..0f0791e752
--- /dev/null
+++ b/docs/pages/base-ui/api/dialog-close.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "DialogClose",
+ "imports": [
+ "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogClose = Dialog.Close;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "DialogClose",
+ "forwardsRefTo": "HTMLButtonElement",
+ "filename": "/packages/mui-base/src/Dialog/Close/DialogClose.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/dialog-description.json b/docs/pages/base-ui/api/dialog-description.json
new file mode 100644
index 0000000000..995ec0670b
--- /dev/null
+++ b/docs/pages/base-ui/api/dialog-description.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "DialogDescription",
+ "imports": [
+ "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogDescription = Dialog.Description;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "DialogDescription",
+ "forwardsRefTo": "HTMLParagraphElement",
+ "filename": "/packages/mui-base/src/Dialog/Description/DialogDescription.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/dialog-popup.json b/docs/pages/base-ui/api/dialog-popup.json
new file mode 100644
index 0000000000..6cb5afbd1f
--- /dev/null
+++ b/docs/pages/base-ui/api/dialog-popup.json
@@ -0,0 +1,21 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "container": { "type": { "name": "union", "description": "HTML element | ref" } },
+ "keepMounted": { "type": { "name": "bool" }, "default": "false" },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "DialogPopup",
+ "imports": [
+ "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogPopup = Dialog.Popup;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "DialogPopup",
+ "forwardsRefTo": "HTMLDivElement",
+ "filename": "/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/dialog-root.json b/docs/pages/base-ui/api/dialog-root.json
new file mode 100644
index 0000000000..478fe8acbe
--- /dev/null
+++ b/docs/pages/base-ui/api/dialog-root.json
@@ -0,0 +1,20 @@
+{
+ "props": {
+ "animated": { "type": { "name": "bool" }, "default": "true" },
+ "defaultOpen": { "type": { "name": "bool" } },
+ "dismissible": { "type": { "name": "bool" }, "default": "true" },
+ "modal": { "type": { "name": "bool" }, "default": "true" },
+ "onOpenChange": { "type": { "name": "func" } },
+ "open": { "type": { "name": "bool" } }
+ },
+ "name": "DialogRoot",
+ "imports": ["import * as Dialog from '@base_ui/react/Dialog';\nconst DialogRoot = Dialog.Root;"],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": null,
+ "muiName": "DialogRoot",
+ "filename": "/packages/mui-base/src/Dialog/Root/DialogRoot.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/dialog-title.json b/docs/pages/base-ui/api/dialog-title.json
new file mode 100644
index 0000000000..c686160ca8
--- /dev/null
+++ b/docs/pages/base-ui/api/dialog-title.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "DialogTitle",
+ "imports": [
+ "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogTitle = Dialog.Title;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "DialogTitle",
+ "forwardsRefTo": "HTMLHeadingElement",
+ "filename": "/packages/mui-base/src/Dialog/Title/DialogTitle.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/dialog-trigger.json b/docs/pages/base-ui/api/dialog-trigger.json
new file mode 100644
index 0000000000..04cc9b2212
--- /dev/null
+++ b/docs/pages/base-ui/api/dialog-trigger.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "DialogTrigger",
+ "imports": [
+ "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogTrigger = Dialog.Trigger;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "DialogTrigger",
+ "forwardsRefTo": "HTMLButtonElement",
+ "filename": "/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/use-dialog-backdrop.json b/docs/pages/base-ui/api/use-dialog-backdrop.json
new file mode 100644
index 0000000000..586c0c6532
--- /dev/null
+++ b/docs/pages/base-ui/api/use-dialog-backdrop.json
@@ -0,0 +1,8 @@
+{
+ "parameters": {},
+ "returnValue": {},
+ "name": "useDialogBackdrop",
+ "filename": "/packages/mui-base/src/Dialog/Backdrop/useDialogBackdrop.ts",
+ "imports": ["import { useDialogBackdrop } from '@base_ui/react/Dialog';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-dialog-close.json b/docs/pages/base-ui/api/use-dialog-close.json
new file mode 100644
index 0000000000..e211a103dc
--- /dev/null
+++ b/docs/pages/base-ui/api/use-dialog-close.json
@@ -0,0 +1,22 @@
+{
+ "parameters": {
+ "onOpenChange": {
+ "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" },
+ "required": true
+ },
+ "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true }
+ },
+ "returnValue": {
+ "getRootProps": {
+ "type": {
+ "name": "(externalProps: React.HTMLAttributes<any>) => React.HTMLAttributes<any>",
+ "description": "(externalProps: React.HTMLAttributes<any>) => React.HTMLAttributes<any>"
+ },
+ "required": true
+ }
+ },
+ "name": "useDialogClose",
+ "filename": "/packages/mui-base/src/Dialog/Close/useDialogClose.ts",
+ "imports": ["import { useDialogClose } from '@base_ui/react/Dialog';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-dialog-popup.json b/docs/pages/base-ui/api/use-dialog-popup.json
new file mode 100644
index 0000000000..de4a07c643
--- /dev/null
+++ b/docs/pages/base-ui/api/use-dialog-popup.json
@@ -0,0 +1,58 @@
+{
+ "parameters": {
+ "animated": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "descriptionElementId": {
+ "type": { "name": "string | undefined", "description": "string | undefined" },
+ "required": true
+ },
+ "isTopmost": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "modal": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "onOpenChange": {
+ "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" },
+ "required": true
+ },
+ "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "ref": {
+ "type": {
+ "name": "React.Ref<HTMLElement>",
+ "description": "React.Ref<HTMLElement>"
+ },
+ "required": true
+ },
+ "setPopupElementId": {
+ "type": {
+ "name": "(id: string | undefined) => void",
+ "description": "(id: string | undefined) => void"
+ },
+ "required": true
+ },
+ "titleElementId": {
+ "type": { "name": "string | undefined", "description": "string | undefined" },
+ "required": true
+ },
+ "dismissible": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" },
+ "id": { "type": { "name": "string", "description": "string" } }
+ },
+ "returnValue": {
+ "floatingContext": {
+ "type": { "name": "FloatingContext", "description": "FloatingContext" },
+ "required": true
+ },
+ "getRootProps": {
+ "type": {
+ "name": "(externalProps: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>",
+ "description": "(externalProps: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>"
+ },
+ "required": true
+ },
+ "mounted": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "transitionStatus": {
+ "type": { "name": "TransitionStatus", "description": "TransitionStatus" },
+ "required": true
+ }
+ },
+ "name": "useDialogPopup",
+ "filename": "/packages/mui-base/src/Dialog/Popup/useDialogPopup.tsx",
+ "imports": ["import { useDialogPopup } from '@base_ui/react/Dialog';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-dialog-root.json b/docs/pages/base-ui/api/use-dialog-root.json
new file mode 100644
index 0000000000..322d329f22
--- /dev/null
+++ b/docs/pages/base-ui/api/use-dialog-root.json
@@ -0,0 +1,82 @@
+{
+ "parameters": {
+ "animated": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" },
+ "defaultOpen": { "type": { "name": "boolean", "description": "boolean" } },
+ "dismissible": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" },
+ "modal": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" },
+ "onNestedDialogClose": { "type": { "name": "() => void", "description": "() => void" } },
+ "onNestedDialogOpen": {
+ "type": {
+ "name": "(ownChildrenCount: number) => void",
+ "description": "(ownChildrenCount: number) => void"
+ }
+ },
+ "onOpenChange": {
+ "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" }
+ },
+ "open": { "type": { "name": "boolean", "description": "boolean" } }
+ },
+ "returnValue": {
+ "descriptionElementId": {
+ "type": { "name": "string | undefined", "description": "string | undefined" },
+ "required": true
+ },
+ "modal": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "nestedOpenDialogCount": {
+ "type": { "name": "number", "description": "number" },
+ "required": true
+ },
+ "onOpenChange": {
+ "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" },
+ "required": true
+ },
+ "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "popupElementId": {
+ "type": { "name": "string | undefined", "description": "string | undefined" },
+ "required": true
+ },
+ "setBackdropPresent": {
+ "type": {
+ "name": "(present: boolean) => void",
+ "description": "(present: boolean) => void"
+ },
+ "required": true
+ },
+ "setDescriptionElementId": {
+ "type": {
+ "name": "(elementId: string | undefined) => void",
+ "description": "(elementId: string | undefined) => void"
+ },
+ "required": true
+ },
+ "setPopupElementId": {
+ "type": {
+ "name": "(elementId: string | undefined) => void",
+ "description": "(elementId: string | undefined) => void"
+ },
+ "required": true
+ },
+ "setTitleElementId": {
+ "type": {
+ "name": "(elementId: string | undefined) => void",
+ "description": "(elementId: string | undefined) => void"
+ },
+ "required": true
+ },
+ "titleElementId": {
+ "type": { "name": "string | undefined", "description": "string | undefined" },
+ "required": true
+ },
+ "onNestedDialogClose": { "type": { "name": "() => void", "description": "() => void" } },
+ "onNestedDialogOpen": {
+ "type": {
+ "name": "(ownChildrenCount: number) => void",
+ "description": "(ownChildrenCount: number) => void"
+ }
+ }
+ },
+ "name": "useDialogRoot",
+ "filename": "/packages/mui-base/src/Dialog/Root/useDialogRoot.ts",
+ "imports": ["import { useDialogRoot } from '@base_ui/react/Dialog';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-dialog-trigger.json b/docs/pages/base-ui/api/use-dialog-trigger.json
new file mode 100644
index 0000000000..db4e01530d
--- /dev/null
+++ b/docs/pages/base-ui/api/use-dialog-trigger.json
@@ -0,0 +1,26 @@
+{
+ "parameters": {
+ "onOpenChange": {
+ "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" },
+ "required": true
+ },
+ "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "popupElementId": {
+ "type": { "name": "string | undefined", "description": "string | undefined" },
+ "required": true
+ }
+ },
+ "returnValue": {
+ "getRootProps": {
+ "type": {
+ "name": "(externalProps?: React.HTMLAttributes<any>) => React.HTMLAttributes<any>",
+ "description": "(externalProps?: React.HTMLAttributes<any>) => React.HTMLAttributes<any>"
+ },
+ "required": true
+ }
+ },
+ "name": "useDialogTrigger",
+ "filename": "/packages/mui-base/src/Dialog/Trigger/useDialogTrigger.ts",
+ "imports": ["import { useDialogTrigger } from '@base_ui/react/Dialog';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-scroll-lock.json b/docs/pages/base-ui/api/use-scroll-lock.json
new file mode 100644
index 0000000000..fd34aaa559
--- /dev/null
+++ b/docs/pages/base-ui/api/use-scroll-lock.json
@@ -0,0 +1,8 @@
+{
+ "parameters": {},
+ "returnValue": {},
+ "name": "useScrollLock",
+ "filename": "/packages/mui-base/src/utils/useScrollLock.ts",
+ "imports": ["import { useScrollLock } from '@base_ui/react/utils';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/react-alert-dialog/[docsTab]/index.js b/docs/pages/base-ui/react-alert-dialog/[docsTab]/index.js
new file mode 100644
index 0000000000..04a6930be0
--- /dev/null
+++ b/docs/pages/base-ui/react-alert-dialog/[docsTab]/index.js
@@ -0,0 +1,106 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/alert-dialog/alert-dialog.md?@mui/markdown';
+import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+import AlertDialogBackdropApiJsonPageContent from '../../api/alert-dialog-backdrop.json';
+import AlertDialogCloseApiJsonPageContent from '../../api/alert-dialog-close.json';
+import AlertDialogDescriptionApiJsonPageContent from '../../api/alert-dialog-description.json';
+import AlertDialogPopupApiJsonPageContent from '../../api/alert-dialog-popup.json';
+import AlertDialogRootApiJsonPageContent from '../../api/alert-dialog-root.json';
+import AlertDialogTitleApiJsonPageContent from '../../api/alert-dialog-title.json';
+import AlertDialogTriggerApiJsonPageContent from '../../api/alert-dialog-trigger.json';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
+
+export const getStaticPaths = () => {
+ return {
+ paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }],
+ fallback: false, // can also be true or 'blocking'
+ };
+};
+
+export const getStaticProps = () => {
+ const AlertDialogBackdropApiReq = require.context(
+ 'docs-base/translations/api-docs/alert-dialog-backdrop',
+ false,
+ /\.\/alert-dialog-backdrop.*.json$/,
+ );
+ const AlertDialogBackdropApiDescriptions = mapApiPageTranslations(AlertDialogBackdropApiReq);
+
+ const AlertDialogCloseApiReq = require.context(
+ 'docs-base/translations/api-docs/alert-dialog-close',
+ false,
+ /\.\/alert-dialog-close.*.json$/,
+ );
+ const AlertDialogCloseApiDescriptions = mapApiPageTranslations(AlertDialogCloseApiReq);
+
+ const AlertDialogDescriptionApiReq = require.context(
+ 'docs-base/translations/api-docs/alert-dialog-description',
+ false,
+ /\.\/alert-dialog-description.*.json$/,
+ );
+ const AlertDialogDescriptionApiDescriptions = mapApiPageTranslations(
+ AlertDialogDescriptionApiReq,
+ );
+
+ const AlertDialogPopupApiReq = require.context(
+ 'docs-base/translations/api-docs/alert-dialog-popup',
+ false,
+ /\.\/alert-dialog-popup.*.json$/,
+ );
+ const AlertDialogPopupApiDescriptions = mapApiPageTranslations(AlertDialogPopupApiReq);
+
+ const AlertDialogRootApiReq = require.context(
+ 'docs-base/translations/api-docs/alert-dialog-root',
+ false,
+ /\.\/alert-dialog-root.*.json$/,
+ );
+ const AlertDialogRootApiDescriptions = mapApiPageTranslations(AlertDialogRootApiReq);
+
+ const AlertDialogTitleApiReq = require.context(
+ 'docs-base/translations/api-docs/alert-dialog-title',
+ false,
+ /\.\/alert-dialog-title.*.json$/,
+ );
+ const AlertDialogTitleApiDescriptions = mapApiPageTranslations(AlertDialogTitleApiReq);
+
+ const AlertDialogTriggerApiReq = require.context(
+ 'docs-base/translations/api-docs/alert-dialog-trigger',
+ false,
+ /\.\/alert-dialog-trigger.*.json$/,
+ );
+ const AlertDialogTriggerApiDescriptions = mapApiPageTranslations(AlertDialogTriggerApiReq);
+
+ return {
+ props: {
+ componentsApiDescriptions: {
+ AlertDialogBackdrop: AlertDialogBackdropApiDescriptions,
+ AlertDialogClose: AlertDialogCloseApiDescriptions,
+ AlertDialogDescription: AlertDialogDescriptionApiDescriptions,
+ AlertDialogPopup: AlertDialogPopupApiDescriptions,
+ AlertDialogRoot: AlertDialogRootApiDescriptions,
+ AlertDialogTitle: AlertDialogTitleApiDescriptions,
+ AlertDialogTrigger: AlertDialogTriggerApiDescriptions,
+ },
+ componentsApiPageContents: {
+ AlertDialogBackdrop: AlertDialogBackdropApiJsonPageContent,
+ AlertDialogClose: AlertDialogCloseApiJsonPageContent,
+ AlertDialogDescription: AlertDialogDescriptionApiJsonPageContent,
+ AlertDialogPopup: AlertDialogPopupApiJsonPageContent,
+ AlertDialogRoot: AlertDialogRootApiJsonPageContent,
+ AlertDialogTitle: AlertDialogTitleApiJsonPageContent,
+ AlertDialogTrigger: AlertDialogTriggerApiJsonPageContent,
+ },
+ hooksApiDescriptions: {},
+ hooksApiPageContents: {},
+ },
+ };
+};
diff --git a/docs/pages/base-ui/react-alert-dialog/index.js b/docs/pages/base-ui/react-alert-dialog/index.js
new file mode 100644
index 0000000000..fe63bb661a
--- /dev/null
+++ b/docs/pages/base-ui/react-alert-dialog/index.js
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/alert-dialog/alert-dialog.md?@mui/markdown';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
diff --git a/docs/pages/base-ui/react-dialog/[docsTab]/index.js b/docs/pages/base-ui/react-dialog/[docsTab]/index.js
new file mode 100644
index 0000000000..db2a57a965
--- /dev/null
+++ b/docs/pages/base-ui/react-dialog/[docsTab]/index.js
@@ -0,0 +1,146 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/dialog/dialog.md?@mui/markdown';
+import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+import DialogBackdropApiJsonPageContent from '../../api/dialog-backdrop.json';
+import DialogCloseApiJsonPageContent from '../../api/dialog-close.json';
+import DialogDescriptionApiJsonPageContent from '../../api/dialog-description.json';
+import DialogPopupApiJsonPageContent from '../../api/dialog-popup.json';
+import DialogRootApiJsonPageContent from '../../api/dialog-root.json';
+import DialogTitleApiJsonPageContent from '../../api/dialog-title.json';
+import DialogTriggerApiJsonPageContent from '../../api/dialog-trigger.json';
+import useDialogCloseApiJsonPageContent from '../../api/use-dialog-close.json';
+import useDialogPopupApiJsonPageContent from '../../api/use-dialog-popup.json';
+import useDialogRootApiJsonPageContent from '../../api/use-dialog-root.json';
+import useDialogTriggerApiJsonPageContent from '../../api/use-dialog-trigger.json';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
+
+export const getStaticPaths = () => {
+ return {
+ paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }],
+ fallback: false, // can also be true or 'blocking'
+ };
+};
+
+export const getStaticProps = () => {
+ const DialogBackdropApiReq = require.context(
+ 'docs-base/translations/api-docs/dialog-backdrop',
+ false,
+ /\.\/dialog-backdrop.*.json$/,
+ );
+ const DialogBackdropApiDescriptions = mapApiPageTranslations(DialogBackdropApiReq);
+
+ const DialogCloseApiReq = require.context(
+ 'docs-base/translations/api-docs/dialog-close',
+ false,
+ /\.\/dialog-close.*.json$/,
+ );
+ const DialogCloseApiDescriptions = mapApiPageTranslations(DialogCloseApiReq);
+
+ const DialogDescriptionApiReq = require.context(
+ 'docs-base/translations/api-docs/dialog-description',
+ false,
+ /\.\/dialog-description.*.json$/,
+ );
+ const DialogDescriptionApiDescriptions = mapApiPageTranslations(DialogDescriptionApiReq);
+
+ const DialogPopupApiReq = require.context(
+ 'docs-base/translations/api-docs/dialog-popup',
+ false,
+ /\.\/dialog-popup.*.json$/,
+ );
+ const DialogPopupApiDescriptions = mapApiPageTranslations(DialogPopupApiReq);
+
+ const DialogRootApiReq = require.context(
+ 'docs-base/translations/api-docs/dialog-root',
+ false,
+ /\.\/dialog-root.*.json$/,
+ );
+ const DialogRootApiDescriptions = mapApiPageTranslations(DialogRootApiReq);
+
+ const DialogTitleApiReq = require.context(
+ 'docs-base/translations/api-docs/dialog-title',
+ false,
+ /\.\/dialog-title.*.json$/,
+ );
+ const DialogTitleApiDescriptions = mapApiPageTranslations(DialogTitleApiReq);
+
+ const DialogTriggerApiReq = require.context(
+ 'docs-base/translations/api-docs/dialog-trigger',
+ false,
+ /\.\/dialog-trigger.*.json$/,
+ );
+ const DialogTriggerApiDescriptions = mapApiPageTranslations(DialogTriggerApiReq);
+
+ const useDialogCloseApiReq = require.context(
+ 'docs-base/translations/api-docs/use-dialog-close',
+ false,
+ /\.\/use-dialog-close.*.json$/,
+ );
+ const useDialogCloseApiDescriptions = mapApiPageTranslations(useDialogCloseApiReq);
+
+ const useDialogPopupApiReq = require.context(
+ 'docs-base/translations/api-docs/use-dialog-popup',
+ false,
+ /\.\/use-dialog-popup.*.json$/,
+ );
+ const useDialogPopupApiDescriptions = mapApiPageTranslations(useDialogPopupApiReq);
+
+ const useDialogRootApiReq = require.context(
+ 'docs-base/translations/api-docs/use-dialog-root',
+ false,
+ /\.\/use-dialog-root.*.json$/,
+ );
+ const useDialogRootApiDescriptions = mapApiPageTranslations(useDialogRootApiReq);
+
+ const useDialogTriggerApiReq = require.context(
+ 'docs-base/translations/api-docs/use-dialog-trigger',
+ false,
+ /\.\/use-dialog-trigger.*.json$/,
+ );
+ const useDialogTriggerApiDescriptions = mapApiPageTranslations(useDialogTriggerApiReq);
+
+ return {
+ props: {
+ componentsApiDescriptions: {
+ DialogBackdrop: DialogBackdropApiDescriptions,
+ DialogClose: DialogCloseApiDescriptions,
+ DialogDescription: DialogDescriptionApiDescriptions,
+ DialogPopup: DialogPopupApiDescriptions,
+ DialogRoot: DialogRootApiDescriptions,
+ DialogTitle: DialogTitleApiDescriptions,
+ DialogTrigger: DialogTriggerApiDescriptions,
+ },
+ componentsApiPageContents: {
+ DialogBackdrop: DialogBackdropApiJsonPageContent,
+ DialogClose: DialogCloseApiJsonPageContent,
+ DialogDescription: DialogDescriptionApiJsonPageContent,
+ DialogPopup: DialogPopupApiJsonPageContent,
+ DialogRoot: DialogRootApiJsonPageContent,
+ DialogTitle: DialogTitleApiJsonPageContent,
+ DialogTrigger: DialogTriggerApiJsonPageContent,
+ },
+ hooksApiDescriptions: {
+ useDialogClose: useDialogCloseApiDescriptions,
+ useDialogPopup: useDialogPopupApiDescriptions,
+ useDialogRoot: useDialogRootApiDescriptions,
+ useDialogTrigger: useDialogTriggerApiDescriptions,
+ },
+ hooksApiPageContents: {
+ useDialogClose: useDialogCloseApiJsonPageContent,
+ useDialogPopup: useDialogPopupApiJsonPageContent,
+ useDialogRoot: useDialogRootApiJsonPageContent,
+ useDialogTrigger: useDialogTriggerApiJsonPageContent,
+ },
+ },
+ };
+};
diff --git a/docs/pages/base-ui/react-dialog/index.js b/docs/pages/base-ui/react-dialog/index.js
new file mode 100644
index 0000000000..8e8543319f
--- /dev/null
+++ b/docs/pages/base-ui/react-dialog/index.js
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/dialog/dialog.md?@mui/markdown';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
diff --git a/docs/pages/experiments/dialog.module.css b/docs/pages/experiments/dialog.module.css
new file mode 100644
index 0000000000..a9f4706804
--- /dev/null
+++ b/docs/pages/experiments/dialog.module.css
@@ -0,0 +1,285 @@
+@keyframes dialog-opening-transform {
+ from {
+ transform: translate(-50%, -35%) scale(0.8) translateY(0);
+ }
+ to {
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ }
+}
+
+@keyframes dialog-opening-opacity {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ visibility: visible;
+ }
+}
+
+@keyframes dialog-closing {
+ from {
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ opacity: 1;
+ visibility: visible;
+ }
+
+ to {
+ transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ opacity: 0.5;
+ visibility: hidden;
+ }
+}
+
+@keyframes backdrop-opening {
+ from {
+ backdrop-filter: blur(1px);
+ opacity: 0;
+ }
+
+ to {
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ visibility: visible;
+ }
+}
+
+@keyframes backdrop-closing {
+ from {
+ opacity: 1;
+ backdrop-filter: blur(6px);
+ }
+
+ to {
+ backdrop-filter: blur(1px);
+ opacity: 0;
+ visibility: hidden;
+ }
+}
+
+.dialog {
+ --transition-duration: 150ms;
+
+ background: #fff;
+ border: 1px solid #f5f5f5;
+ min-width: 300px;
+ max-width: 500px;
+ border-radius: 4px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ padding: 16px;
+ font-family: IBM Plex Sans;
+ z-index: 1;
+ transform: translate(-50%, -50%);
+ opacity: calc(pow(0.95, var(--nested-dialogs)));
+
+ &.withTransitions {
+ transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ visibility: hidden;
+ opacity: 0;
+ transition:
+ transform var(--transition-duration) ease-in,
+ opacity var(--transition-duration) ease-in,
+ visibility var(--transition-duration) step-end;
+
+ &[data-state='open'] {
+ @starting-style {
+ & {
+ transform: translate(-50%, -35%) scale(0.8) translateY(0);
+ opacity: 0;
+ }
+ }
+
+ visibility: visible;
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ transition:
+ transform var(--transition-duration) ease-out,
+ opacity var(--transition-duration) ease-out,
+ visibility var(--transition-duration) step-start;
+ }
+ }
+
+ &.withAnimations {
+ transform: translate(-50%, -35%) scale(0.8, 0.9) translateY(0);
+ visibility: hidden;
+ opacity: 0;
+
+ &[data-state='open'] {
+ animation:
+ dialog-opening-transform var(--transition-duration) ease-out,
+ dialog-opening-opacity var(--transition-duration) ease-out forwards;
+ transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs))))
+ translateY(calc(-30px * var(--nested-dialogs)));
+ transition: transform var(--transition-duration) ease-out;
+ }
+
+ &[data-exiting] {
+ animation: dialog-closing var(--transition-duration) ease-in forwards;
+ }
+ }
+
+ &.withReactSpringTransition {
+ top: 50vh;
+ left: 50vw;
+ visibility: visible;
+ opacity: 1;
+ }
+}
+
+.backdrop {
+ background: radial-gradient(#cecdcf36, #8b94ab47);
+ z-index: 0;
+ position: fixed;
+ inset: 0;
+ opacity: 0;
+ visibility: hidden;
+ backdrop-filter: blur(1px);
+
+ &.withTransitions {
+ transition:
+ backdrop-filter 300ms ease-in,
+ opacity 300ms ease-in,
+ visibility 300ms step-end;
+
+ &[data-state='open'] {
+ @starting-style {
+ & {
+ opacity: 0;
+ backdrop-filter: blur(1px);
+ }
+ }
+
+ backdrop-filter: blur(6px);
+ opacity: 1;
+ visibility: visible;
+ transition:
+ backdrop-filter 500ms ease-out,
+ opacity 500ms ease-out;
+ }
+ }
+
+ &.withAnimations {
+ &[data-state='open'] {
+ animation: backdrop-opening 500ms ease-out forwards;
+ }
+
+ &[data-exiting] {
+ animation: backdrop-closing 500ms ease-in forwards;
+ }
+ }
+}
+
+.title {
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+.page {
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: 16px;
+ font-family: IBM Plex Sans;
+
+ h1 {
+ font-family: General Sans;
+ font-weight: 600;
+ font-size: 2rem;
+ }
+
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ }
+
+ label {
+ font-size: 0.8333rem;
+ }
+
+ label + label {
+ margin-left: 16px;
+ }
+}
+
+.springWrapper {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+
+.button {
+ background: #eee;
+ padding: 8px 16px;
+ border: 1px solid #d8d8d8;
+ border-radius: 4px;
+ font-family: inherit;
+
+ &:hover {
+ background: #ffbf2b;
+ }
+
+ &:focus-visible {
+ outline: 2px solid #ffbf2b;
+ }
+
+ &:active {
+ background: #cc9922;
+ border-color: #cc9922;
+
+ &:focus-visible {
+ outline-color: #cc9922;
+ }
+ }
+}
+
+.form {
+ display: flex;
+ gap: 16px;
+ margin-top: 24px;
+ flex-wrap: wrap;
+
+ input[type='text'],
+ textarea {
+ padding: 8px;
+ border: 1px solid #d8d8d8;
+ border-radius: 4px;
+ font-family: inherit;
+ box-sizing: border-box;
+ }
+
+ textarea {
+ resize: vertical;
+ min-height: 100px;
+ width: 100%;
+ }
+
+ & > * {
+ flex: 1 0 auto;
+ margin: 0;
+ }
+}
+
+.controls {
+ display: flex;
+ gap: 16px;
+ margin-top: 16px;
+ border-top: 1px solid #d8d8d8;
+ padding-top: 16px;
+
+ & > * {
+ flex: 1 1 50%;
+ margin: 0;
+ }
+}
+
+.demo {
+ margin-right: 8px;
+}
diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx
new file mode 100644
index 0000000000..3244285b89
--- /dev/null
+++ b/docs/pages/experiments/dialog.tsx
@@ -0,0 +1,225 @@
+import * as React from 'react';
+import clsx from 'clsx';
+import * as Dialog from '@base_ui/react/Dialog';
+// eslint-disable-next-line no-restricted-imports
+import { useTransitionStatus } from '@base_ui/react/utils/useTransitionStatus';
+import { animated as springAnimated, useSpring, useSpringRef } from '@react-spring/web';
+import classes from './dialog.module.css';
+
+const NESTED_DIALOGS = 8;
+
+interface DemoProps {
+ keepMounted: boolean;
+ modal: boolean;
+ dismissible: boolean;
+}
+
+function renderContent(
+ title: string,
+ includeNested: number,
+ nestedClassName: string,
+ modal: boolean,
+ dismissible: boolean,
+) {
+ return (
+
+ {title}
+ This is a sample dialog.
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget sapien id dolor rutrum
+ porta. Sed enim nulla, placerat eu tincidunt non, ultrices in lectus. Curabitur pellentesque
+ diam nec ligula hendrerit dapibus.
+
+
+
+
+
+
+
+
+
+ {includeNested > 0 ? (
+
+ Open nested
+
+
+ {renderContent(
+ `Nested dialog ${NESTED_DIALOGS + 1 - includeNested}`,
+ includeNested - 1,
+ nestedClassName,
+ modal,
+ dismissible,
+ )}
+
+
+ ) : null}
+
+ Close
+
+
+ );
+}
+
+function CssTransitionDialogDemo({ keepMounted, modal, dismissible }: DemoProps) {
+ return (
+
+
+ Open with CSS transition
+
+
+
+
+ {renderContent(
+ 'Dialog with CSS transitions',
+ NESTED_DIALOGS,
+ classes.withTransitions,
+ modal,
+ dismissible,
+ )}
+
+
+
+ );
+}
+
+function CssAnimationDialogDemo({ keepMounted, modal, dismissible }: DemoProps) {
+ return (
+
+
+ Open with CSS animation
+
+
+
+
+ {renderContent(
+ 'Dialog with CSS animations',
+ NESTED_DIALOGS,
+ classes.withAnimations,
+ modal,
+ dismissible,
+ )}
+
+
+
+ );
+}
+
+// @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function ReactSpringDialogDemo({ animated, keepMounted, modal, dismissible }: DemoProps) {
+ const [open, setOpen] = React.useState(false);
+
+ return (
+
+
+
+ Open with React Spring transition
+
+
+
+
+
+
+ {renderContent(
+ 'Dialog with ReactSpring transitions',
+ 3,
+ classes.withReactSpringTransition,
+ modal,
+ dismissible,
+ )}
+
+
+
+
+ );
+}
+
+function ReactSpringTransition(props: { open: boolean; children?: React.ReactElement }) {
+ const { open, children } = props;
+
+ const api = useSpringRef();
+ const springs = useSpring({
+ ref: api,
+ from: { opacity: 0, transform: 'translateY(-8px) scale(0.95)' },
+ });
+
+ const { mounted, setMounted } = useTransitionStatus(open);
+
+ React.useEffect(() => {
+ if (open) {
+ api.start({
+ opacity: 1,
+ transform: 'translateY(0) scale(1)',
+ config: { tension: 250, friction: 10 },
+ });
+ } else {
+ api.start({
+ opacity: 0,
+ transform: 'translateY(-8px) scale(0.95)',
+ config: { tension: 170, friction: 26 },
+ onRest: () => setMounted(false),
+ });
+ }
+ }, [api, open, mounted, setMounted]);
+
+ return mounted ? (
+
+ {children}
+
+ ) : null;
+}
+
+export default function DialogExperiment() {
+ const [keepMounted, setKeepMounted] = React.useState(false);
+ const [modal, setModal] = React.useState(true);
+ const [dismissible, setDismissible] = React.useState(false);
+
+ return (
+
+
Dialog
+
+
+
+ Options
+
+ setKeepMounted(event.target.checked)}
+ />{' '}
+ Keep mounted
+
+
+ setModal(event.target.checked)}
+ />{' '}
+ Modal
+
+
+ setDismissible(event.target.checked)}
+ />{' '}
+ Soft-close
+
+
+ );
+}
diff --git a/docs/translations/api-docs/alert-dialog-backdrop/alert-dialog-backdrop.json b/docs/translations/api-docs/alert-dialog-backdrop/alert-dialog-backdrop.json
new file mode 100644
index 0000000000..0b0d36e82d
--- /dev/null
+++ b/docs/translations/api-docs/alert-dialog-backdrop/alert-dialog-backdrop.json
@@ -0,0 +1,13 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "keepMounted": {
+ "description": "If true
, the backdrop element is kept in the DOM when closed."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/alert-dialog-close/alert-dialog-close.json b/docs/translations/api-docs/alert-dialog-close/alert-dialog-close.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/alert-dialog-close/alert-dialog-close.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/alert-dialog-description/alert-dialog-description.json b/docs/translations/api-docs/alert-dialog-description/alert-dialog-description.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/alert-dialog-description/alert-dialog-description.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/alert-dialog-popup/alert-dialog-popup.json b/docs/translations/api-docs/alert-dialog-popup/alert-dialog-popup.json
new file mode 100644
index 0000000000..e91f99d7cc
--- /dev/null
+++ b/docs/translations/api-docs/alert-dialog-popup/alert-dialog-popup.json
@@ -0,0 +1,14 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "container": { "description": "The container element to which the popup is appended to." },
+ "keepMounted": {
+ "description": "If true
, the dialog element is kept in the DOM when closed."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/alert-dialog-root/alert-dialog-root.json b/docs/translations/api-docs/alert-dialog-root/alert-dialog-root.json
new file mode 100644
index 0000000000..7b8aac1e0f
--- /dev/null
+++ b/docs/translations/api-docs/alert-dialog-root/alert-dialog-root.json
@@ -0,0 +1,16 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "animated": {
+ "description": "If true
, the dialog supports CSS-based animations and transitions. It is kept in the DOM until the animation completes."
+ },
+ "defaultOpen": {
+ "description": "Determines whether the dialog is initally open. This is an uncontrolled equivalent of the open
prop."
+ },
+ "onOpenChange": {
+ "description": "Callback invoked when the dialog is being opened or closed."
+ },
+ "open": { "description": "Determines whether the dialog is open." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/alert-dialog-title/alert-dialog-title.json b/docs/translations/api-docs/alert-dialog-title/alert-dialog-title.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/alert-dialog-title/alert-dialog-title.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/alert-dialog-trigger/alert-dialog-trigger.json b/docs/translations/api-docs/alert-dialog-trigger/alert-dialog-trigger.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/alert-dialog-trigger/alert-dialog-trigger.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/dialog-backdrop/dialog-backdrop.json b/docs/translations/api-docs/dialog-backdrop/dialog-backdrop.json
new file mode 100644
index 0000000000..0b0d36e82d
--- /dev/null
+++ b/docs/translations/api-docs/dialog-backdrop/dialog-backdrop.json
@@ -0,0 +1,13 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "keepMounted": {
+ "description": "If true
, the backdrop element is kept in the DOM when closed."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/dialog-close/dialog-close.json b/docs/translations/api-docs/dialog-close/dialog-close.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/dialog-close/dialog-close.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/dialog-description/dialog-description.json b/docs/translations/api-docs/dialog-description/dialog-description.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/dialog-description/dialog-description.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/dialog-popup/dialog-popup.json b/docs/translations/api-docs/dialog-popup/dialog-popup.json
new file mode 100644
index 0000000000..e91f99d7cc
--- /dev/null
+++ b/docs/translations/api-docs/dialog-popup/dialog-popup.json
@@ -0,0 +1,14 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "container": { "description": "The container element to which the popup is appended to." },
+ "keepMounted": {
+ "description": "If true
, the dialog element is kept in the DOM when closed."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/dialog-root/dialog-root.json b/docs/translations/api-docs/dialog-root/dialog-root.json
new file mode 100644
index 0000000000..7a713035df
--- /dev/null
+++ b/docs/translations/api-docs/dialog-root/dialog-root.json
@@ -0,0 +1,20 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "animated": {
+ "description": "If true
, the dialog supports CSS-based animations and transitions. It is kept in the DOM until the animation completes."
+ },
+ "defaultOpen": {
+ "description": "Determines whether the dialog is initally open. This is an uncontrolled equivalent of the open
prop."
+ },
+ "dismissible": {
+ "description": "Determines whether the dialog should close when clicking outside of it."
+ },
+ "modal": { "description": "Determines whether the dialog is modal." },
+ "onOpenChange": {
+ "description": "Callback invoked when the dialog is being opened or closed."
+ },
+ "open": { "description": "Determines whether the dialog is open." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/dialog-title/dialog-title.json b/docs/translations/api-docs/dialog-title/dialog-title.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/dialog-title/dialog-title.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/dialog-trigger/dialog-trigger.json b/docs/translations/api-docs/dialog-trigger/dialog-trigger.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/dialog-trigger/dialog-trigger.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/use-dialog-backdrop/use-dialog-backdrop.json b/docs/translations/api-docs/use-dialog-backdrop/use-dialog-backdrop.json
new file mode 100644
index 0000000000..e3eb65c6e4
--- /dev/null
+++ b/docs/translations/api-docs/use-dialog-backdrop/use-dialog-backdrop.json
@@ -0,0 +1 @@
+{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} }
diff --git a/docs/translations/api-docs/use-dialog-close/use-dialog-close.json b/docs/translations/api-docs/use-dialog-close/use-dialog-close.json
new file mode 100644
index 0000000000..7525dba558
--- /dev/null
+++ b/docs/translations/api-docs/use-dialog-close/use-dialog-close.json
@@ -0,0 +1,12 @@
+{
+ "hookDescription": "",
+ "parametersDescriptions": {
+ "onOpenChange": {
+ "description": "Callback invoked when the dialog is being opened or closed."
+ },
+ "open": { "description": "Determines whether the dialog is open." }
+ },
+ "returnValueDescriptions": {
+ "getRootProps": { "description": "Resolver for the root element props." }
+ }
+}
diff --git a/docs/translations/api-docs/use-dialog-popup/use-dialog-popup.json b/docs/translations/api-docs/use-dialog-popup/use-dialog-popup.json
new file mode 100644
index 0000000000..0e9d471422
--- /dev/null
+++ b/docs/translations/api-docs/use-dialog-popup/use-dialog-popup.json
@@ -0,0 +1,34 @@
+{
+ "hookDescription": "",
+ "parametersDescriptions": {
+ "animated": {
+ "description": "If true
, the dialog supports CSS-based animations and transitions. It is kept in the DOM until the animation completes."
+ },
+ "descriptionElementId": {
+ "description": "The id of the description element associated with the dialog."
+ },
+ "dismissible": {
+ "description": "Determines whether the dialog should close when clicking outside of it."
+ },
+ "id": { "description": "The id of the dialog element." },
+ "isTopmost": { "description": "Determines if the dialog is the top-most one." },
+ "modal": { "description": "Determines if the dialog is modal." },
+ "onOpenChange": {
+ "description": "Callback fired when the dialog is requested to be opened or closed."
+ },
+ "open": { "description": "Determines if the dialog is open." },
+ "ref": { "description": "The ref to the dialog element." },
+ "setPopupElementId": { "description": "Callback to set the id of the popup element." },
+ "titleElementId": { "description": "The id of the title element associated with the dialog." }
+ },
+ "returnValueDescriptions": {
+ "floatingContext": {
+ "description": "Floating UI context for the dialog's FloatingFocusManager."
+ },
+ "getRootProps": { "description": "Resolver for the root element props." },
+ "mounted": {
+ "description": "Determines if the dialog should be mounted even if closed (as the exit animation is still in progress)."
+ },
+ "transitionStatus": { "description": "The current transition status of the dialog." }
+ }
+}
diff --git a/docs/translations/api-docs/use-dialog-root/use-dialog-root.json b/docs/translations/api-docs/use-dialog-root/use-dialog-root.json
new file mode 100644
index 0000000000..2307cb1169
--- /dev/null
+++ b/docs/translations/api-docs/use-dialog-root/use-dialog-root.json
@@ -0,0 +1,44 @@
+{
+ "hookDescription": "",
+ "parametersDescriptions": {
+ "animated": {
+ "description": "If true
, the dialog supports CSS-based animations and transitions. It is kept in the DOM until the animation completes."
+ },
+ "defaultOpen": {
+ "description": "Determines whether the dialog is initally open. This is an uncontrolled equivalent of the open
prop."
+ },
+ "dismissible": {
+ "description": "Determines whether the dialog should close when clicking outside of it."
+ },
+ "modal": { "description": "Determines whether the dialog is modal." },
+ "onNestedDialogClose": { "description": "Callback to invoke when a nested dialog is closed." },
+ "onNestedDialogOpen": { "description": "Callback to invoke when a nested dialog is opened." },
+ "onOpenChange": {
+ "description": "Callback invoked when the dialog is being opened or closed."
+ },
+ "open": { "description": "Determines whether the dialog is open." }
+ },
+ "returnValueDescriptions": {
+ "descriptionElementId": {
+ "description": "The id of the description element associated with the dialog."
+ },
+ "modal": { "description": "Determines if the dialog is modal." },
+ "nestedOpenDialogCount": { "description": "Number of nested dialogs that are currently open." },
+ "onNestedDialogClose": { "description": "Callback to invoke when a nested dialog is closed." },
+ "onNestedDialogOpen": { "description": "Callback to invoke when a nested dialog is opened." },
+ "onOpenChange": {
+ "description": "Callback to fire when the dialog is requested to be opened or closed."
+ },
+ "open": { "description": "Determines if the dialog is open." },
+ "popupElementId": { "description": "The id of the popup element." },
+ "setBackdropPresent": {
+ "description": "Callback to notify the dialog that the backdrop is present."
+ },
+ "setDescriptionElementId": {
+ "description": "Callback to set the id of the description element associated with the dialog."
+ },
+ "setPopupElementId": { "description": "Callback to set the id of the popup element." },
+ "setTitleElementId": { "description": "Callback to set the id of the title element." },
+ "titleElementId": { "description": "The id of the title element associated with the dialog." }
+ }
+}
diff --git a/docs/translations/api-docs/use-dialog-trigger/use-dialog-trigger.json b/docs/translations/api-docs/use-dialog-trigger/use-dialog-trigger.json
new file mode 100644
index 0000000000..88b38c9d8e
--- /dev/null
+++ b/docs/translations/api-docs/use-dialog-trigger/use-dialog-trigger.json
@@ -0,0 +1,13 @@
+{
+ "hookDescription": "",
+ "parametersDescriptions": {
+ "onOpenChange": {
+ "description": "Callback to fire when the dialog is requested to be opened or closed."
+ },
+ "open": { "description": "Determines if the dialog is open." },
+ "popupElementId": { "description": "The id of the popup element." }
+ },
+ "returnValueDescriptions": {
+ "getRootProps": { "description": "Resolver for the root element props." }
+ }
+}
diff --git a/docs/translations/api-docs/use-scroll-lock/use-scroll-lock.json b/docs/translations/api-docs/use-scroll-lock/use-scroll-lock.json
new file mode 100644
index 0000000000..fbdab840bd
--- /dev/null
+++ b/docs/translations/api-docs/use-scroll-lock/use-scroll-lock.json
@@ -0,0 +1,5 @@
+{
+ "hookDescription": "Locks the scroll of the document when enabled.",
+ "parametersDescriptions": {},
+ "returnValueDescriptions": {}
+}
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index 569c621e49..2918989122 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -225,6 +225,9 @@
"/base-ui/react-switch": "Switch",
"data-display": "Data display",
"/base-ui/react-tooltip": "Tooltip",
+ "feedback": "Feedback",
+ "/base-ui/react-alert-dialog": "Alert Dialog",
+ "/base-ui/react-dialog": "Dialog",
"navigation": "Navigation",
"/base-ui/react-tabs": "Tabs",
"/base-ui/guides": "How-to guides",
diff --git a/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.test.tsx b/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.test.tsx
new file mode 100644
index 0000000000..8c7bc4821a
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.test.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { createRenderer, describeConformance } from '../../../test';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLDivElement,
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+
+ it('has role="presentation"', async () => {
+ const { getByTestId } = await render(
+
+
+ ,
+ );
+
+ expect(getByTestId('backdrop')).to.have.attribute('role', 'presentation');
+ });
+});
diff --git a/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.tsx b/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.tsx
new file mode 100644
index 0000000000..ac29d6428c
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.tsx
@@ -0,0 +1,89 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingPortal } from '@floating-ui/react';
+import type {
+ AlertDialogBackdropOwnerState,
+ AlertDialogBackdropProps,
+} from './AlertDialogBackdrop.types';
+import { useAlertDialogRootContext } from '../Root/AlertDialogRootContext';
+import { useDialogBackdrop } from '../../Dialog/Backdrop/useDialogBackdrop';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+
+const AlertDialogBackdrop = React.forwardRef(function AlertDialogBackdrop(
+ props: AlertDialogBackdropProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, keepMounted = false, ...other } = props;
+ const { open, hasParentDialog, setBackdropPresent, animated } = useAlertDialogRootContext();
+
+ const handleMount = React.useCallback(() => setBackdropPresent(true), [setBackdropPresent]);
+ const handleUnmount = React.useCallback(() => setBackdropPresent(false), [setBackdropPresent]);
+
+ const { getRootProps, mounted, transitionStatus } = useDialogBackdrop({
+ animated,
+ open,
+ ref: forwardedRef,
+ onMount: handleMount,
+ onUnmount: handleUnmount,
+ });
+
+ const ownerState: AlertDialogBackdropOwnerState = { open, transitionStatus };
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'div',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ extraProps: other,
+ customStyleHookMapping: {
+ open: (value) => ({ 'data-state': value ? 'open' : 'closed' }),
+ transitionStatus: (value) => {
+ if (value === 'entering') {
+ return { 'data-entering': '' } as Record;
+ }
+ if (value === 'exiting') {
+ return { 'data-exiting': '' };
+ }
+ return null;
+ },
+ },
+ });
+
+ if (!mounted && !keepMounted) {
+ return null;
+ }
+
+ if (hasParentDialog) {
+ // no need to render nested backdrops
+ return null;
+ }
+
+ return {renderElement()} ;
+});
+
+AlertDialogBackdrop.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * If `true`, the backdrop element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { AlertDialogBackdrop };
diff --git a/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.types.ts b/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.types.ts
new file mode 100644
index 0000000000..85e9c416d1
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.types.ts
@@ -0,0 +1,17 @@
+import { TransitionStatus } from '../../utils/useTransitionStatus';
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface AlertDialogBackdropProps
+ extends BaseUIComponentProps<'div', AlertDialogBackdropOwnerState> {
+ /**
+ * If `true`, the backdrop element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted?: boolean;
+}
+
+export interface AlertDialogBackdropOwnerState {
+ open: boolean;
+ transitionStatus: TransitionStatus;
+}
diff --git a/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.test.tsx b/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.test.tsx
new file mode 100644
index 0000000000..f9badc3bb2
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { createRenderer, describeConformance } from '../../../test';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLButtonElement,
+ render: (node) => {
+ return render(
+
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.tsx b/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.tsx
new file mode 100644
index 0000000000..ef72b256aa
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { AlertDialogCloseOwnerState, AlertDialogCloseProps } from './AlertDialogClose.types';
+import { useAlertDialogRootContext } from '../Root/AlertDialogRootContext';
+import { useDialogClose } from '../../Dialog/Close/useDialogClose';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+
+const AlertDialogClose = React.forwardRef(function AlertDialogClose(
+ props: AlertDialogCloseProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, ...other } = props;
+ const { open, onOpenChange } = useAlertDialogRootContext();
+ const { getRootProps } = useDialogClose({ open, onOpenChange });
+
+ const ownerState: AlertDialogCloseOwnerState = {
+ open,
+ };
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'button',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ ref: forwardedRef,
+ extraProps: other,
+ });
+
+ return renderElement();
+});
+
+AlertDialogClose.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { AlertDialogClose };
diff --git a/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.types.ts b/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.types.ts
new file mode 100644
index 0000000000..2e8e4f2df1
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.types.ts
@@ -0,0 +1,8 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface AlertDialogCloseProps
+ extends BaseUIComponentProps<'button', AlertDialogCloseOwnerState> {}
+
+export interface AlertDialogCloseOwnerState {
+ open: boolean;
+}
diff --git a/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.test.tsx b/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.test.tsx
new file mode 100644
index 0000000000..f4e3cb10e4
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { createRenderer, describeConformance } from '../../../test';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLParagraphElement,
+ render: (node) => {
+ return render(
+
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.tsx b/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.tsx
new file mode 100644
index 0000000000..8c97ef167e
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type {
+ AlertDialogDescriptionOwnerState,
+ AlertDialogDescriptionProps,
+} from './AlertDialogDescription.types';
+import { useAlertDialogRootContext } from '../Root/AlertDialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
+import { useId } from '../../utils/useId';
+
+const AlertDialogDescription = React.forwardRef(function AlertDialogDescription(
+ props: AlertDialogDescriptionProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, id: idProp, ...other } = props;
+ const { setDescriptionElementId, open } = useAlertDialogRootContext();
+
+ const ownerState: AlertDialogDescriptionOwnerState = {
+ open,
+ };
+
+ const id = useId(idProp);
+
+ useEnhancedEffect(() => {
+ setDescriptionElementId(id);
+ return () => {
+ setDescriptionElementId(undefined);
+ };
+ }, [id, setDescriptionElementId]);
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'p',
+ className,
+ ownerState,
+ ref: forwardedRef,
+ extraProps: other,
+ });
+
+ return renderElement();
+});
+
+AlertDialogDescription.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * @ignore
+ */
+ id: PropTypes.string,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { AlertDialogDescription };
diff --git a/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.types.ts b/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.types.ts
new file mode 100644
index 0000000000..cf605152b6
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.types.ts
@@ -0,0 +1,8 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface AlertDialogDescriptionProps
+ extends BaseUIComponentProps<'p', AlertDialogDescriptionOwnerState> {}
+
+export interface AlertDialogDescriptionOwnerState {
+ open: boolean;
+}
diff --git a/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.test.tsx b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.test.tsx
new file mode 100644
index 0000000000..48f59b1684
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.test.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { createRenderer, describeConformance } from '../../../test';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLDivElement,
+ render: (node) => {
+ return render(
+
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+
+ it('should have role="alertdialog"', async () => {
+ const { getByTestId } = await render(
+
+
+
+ ,
+ );
+
+ const dialog = getByTestId('test-alert-dialog');
+ expect(dialog).to.have.attribute('role', 'alertdialog');
+ });
+});
diff --git a/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx
new file mode 100644
index 0000000000..a68aa4f8d9
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx
@@ -0,0 +1,104 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingFocusManager, FloatingPortal } from '@floating-ui/react';
+import { AlertDialogPopupOwnerState, AlertDialogPopupProps } from './AlertDialogPopup.types';
+import { useDialogPopup } from '../../Dialog/Popup/useDialogPopup';
+import { useAlertDialogRootContext } from '../Root/AlertDialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { refType, HTMLElementType } from '../../utils/proptypes';
+
+const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
+ props: AlertDialogPopupProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, container, id, keepMounted = false, render, ...other } = props;
+
+ const rootContext = useAlertDialogRootContext();
+ const { open, nestedOpenDialogCount } = rootContext;
+
+ const { getRootProps, floatingContext, mounted, transitionStatus } = useDialogPopup({
+ id,
+ ref: forwardedRef,
+ dismissible: false,
+ isTopmost: nestedOpenDialogCount === 0,
+ ...rootContext,
+ });
+
+ const ownerState: AlertDialogPopupOwnerState = {
+ open,
+ nestedOpenDialogCount,
+ transitionStatus,
+ };
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'div',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ extraProps: {
+ ...other,
+ style: { '--nested-dialogs': nestedOpenDialogCount },
+ role: 'alertdialog',
+ },
+ customStyleHookMapping: {
+ open: (value) => ({ 'data-state': value ? 'open' : 'closed' }),
+ nestedOpenDialogCount: (value) => ({ 'data-nested-dialogs': value.toString() }),
+ transitionStatus: (value) => {
+ if (value === 'entering') {
+ return { 'data-entering': '' } as Record;
+ }
+ if (value === 'exiting') {
+ return { 'data-exiting': '' };
+ }
+ return null;
+ },
+ },
+ });
+
+ if (!keepMounted && !mounted) {
+ return null;
+ }
+
+ return (
+
+
+ {renderElement()}
+
+
+ );
+});
+
+AlertDialogPopup.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * The container element to which the popup is appended to.
+ */
+ container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, refType]),
+ /**
+ * @ignore
+ */
+ id: PropTypes.string,
+ /**
+ * If `true`, the dialog element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { AlertDialogPopup };
diff --git a/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.types.ts b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.types.ts
new file mode 100644
index 0000000000..392b6787df
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.types.ts
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import { type BaseUIComponentProps } from '../../utils/types';
+import { TransitionStatus } from '../../utils/useTransitionStatus';
+
+export interface AlertDialogPopupProps
+ extends BaseUIComponentProps<'div', AlertDialogPopupOwnerState> {
+ /**
+ * The container element to which the popup is appended to.
+ */
+ container?: HTMLElement | null | React.MutableRefObject;
+ /**
+ * If `true`, the dialog element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted?: boolean;
+}
+
+export interface AlertDialogPopupOwnerState {
+ open: boolean;
+ nestedOpenDialogCount: number;
+ transitionStatus: TransitionStatus;
+}
diff --git a/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.ts b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.ts
new file mode 100644
index 0000000000..7d85215e80
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.ts
@@ -0,0 +1 @@
+// This file must be present for the doc gen to work
diff --git a/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.tsx b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.tsx
new file mode 100644
index 0000000000..4e0fffc9d9
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { AlertDialogRootProps } from './AlertDialogRoot.types';
+import { AlertDialogRootContext } from './AlertDialogRootContext';
+import { useDialogRoot } from '../../Dialog/Root/useDialogRoot';
+
+function AlertDialogRoot(props: AlertDialogRootProps) {
+ const { children, defaultOpen, onOpenChange, open: openProp, animated = true } = props;
+
+ const dialogRootContext = React.useContext(AlertDialogRootContext);
+
+ const dialogRoot = useDialogRoot({
+ open: openProp,
+ defaultOpen,
+ onOpenChange,
+ modal: true,
+ onNestedDialogClose: dialogRootContext?.onNestedDialogClose,
+ onNestedDialogOpen: dialogRootContext?.onNestedDialogOpen,
+ });
+
+ const hasParentDialog = Boolean(dialogRootContext);
+
+ const contextValue = React.useMemo(
+ () => ({ ...dialogRoot, hasParentDialog, animated }),
+ [dialogRoot, hasParentDialog, animated],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+AlertDialogRoot.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * If `true`, the dialog supports CSS-based animations and transitions.
+ * It is kept in the DOM until the animation completes.
+ *
+ * @default true
+ */
+ animated: PropTypes.bool,
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Determines whether the dialog is initally open.
+ * This is an uncontrolled equivalent of the `open` prop.
+ */
+ defaultOpen: PropTypes.bool,
+ /**
+ * Callback invoked when the dialog is being opened or closed.
+ */
+ onOpenChange: PropTypes.func,
+ /**
+ * Determines whether the dialog is open.
+ */
+ open: PropTypes.bool,
+} as any;
+
+export { AlertDialogRoot };
diff --git a/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.types.ts b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.types.ts
new file mode 100644
index 0000000000..96ffc75941
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.types.ts
@@ -0,0 +1,15 @@
+import type { DialogRootProps, UseDialogRootReturnValue } from '../../Dialog/Root/DialogRoot.types';
+
+export type AlertDialogRootProps = Omit;
+
+export interface AlertDialogRootContextValue extends UseDialogRootReturnValue {
+ /**
+ * If `true`, the dialog supports CSS-based animations and transitions.
+ * It is kept in the DOM until the animation completes.
+ */
+ animated: boolean;
+ /**
+ * Determines if the dialog is nested within a parent dialog.
+ */
+ hasParentDialog: boolean;
+}
diff --git a/packages/mui-base/src/AlertDialog/Root/AlertDialogRootContext.ts b/packages/mui-base/src/AlertDialog/Root/AlertDialogRootContext.ts
new file mode 100644
index 0000000000..19423d2d48
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Root/AlertDialogRootContext.ts
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import type { AlertDialogRootContextValue } from './AlertDialogRoot.types';
+
+export const AlertDialogRootContext = React.createContext(
+ undefined,
+);
+
+export function useAlertDialogRootContext() {
+ const context = React.useContext(AlertDialogRootContext);
+ if (context === undefined) {
+ throw new Error('useAlertDialogRootContext must be used within an AlertDialogRoot');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.test.tsx b/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.test.tsx
new file mode 100644
index 0000000000..d8f41b4fab
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { createRenderer, describeConformance } from '../../../test';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLHeadingElement,
+ render: (node) => {
+ return render(
+
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.tsx b/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.tsx
new file mode 100644
index 0000000000..e5301b06c1
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.tsx
@@ -0,0 +1,63 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type { AlertDialogTitleOwnerState, AlertDialogTitleProps } from './AlertDialogTitle.types';
+import { useAlertDialogRootContext } from '../Root/AlertDialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
+import { useId } from '../../utils/useId';
+
+const AlertDialogTitle = React.forwardRef(function AlertDialogTitle(
+ props: AlertDialogTitleProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, id: idProp, ...other } = props;
+ const { setTitleElementId, open } = useAlertDialogRootContext();
+
+ const ownerState: AlertDialogTitleOwnerState = {
+ open,
+ };
+
+ const id = useId(idProp);
+
+ useEnhancedEffect(() => {
+ setTitleElementId(id);
+ return () => {
+ setTitleElementId(undefined);
+ };
+ }, [id, setTitleElementId]);
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'h2',
+ className,
+ ownerState,
+ ref: forwardedRef,
+ extraProps: other,
+ });
+
+ return renderElement();
+});
+
+AlertDialogTitle.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * @ignore
+ */
+ id: PropTypes.string,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { AlertDialogTitle };
diff --git a/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.types.ts b/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.types.ts
new file mode 100644
index 0000000000..bd2ec66bfb
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.types.ts
@@ -0,0 +1,8 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface AlertDialogTitleProps
+ extends BaseUIComponentProps<'h2', AlertDialogTitleOwnerState> {}
+
+export interface AlertDialogTitleOwnerState {
+ open: boolean;
+}
diff --git a/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.test.tsx b/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.test.tsx
new file mode 100644
index 0000000000..513a6d57a2
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import * as AlertDialog from '@base_ui/react/AlertDialog';
+import { createRenderer, describeConformance } from '../../../test';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLButtonElement,
+ render: (node) => {
+ return render(
+
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.tsx b/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.tsx
new file mode 100644
index 0000000000..80b76094b6
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useDialogTrigger } from '../../Dialog/Trigger/useDialogTrigger';
+import type {
+ AlertDialogTriggerOwnerState,
+ AlertDialogTriggerProps,
+} from './AlertDialogTrigger.types';
+import { useAlertDialogRootContext } from '../Root/AlertDialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+
+const AlertDialogTrigger = React.forwardRef(function AlertDialogTrigger(
+ props: AlertDialogTriggerProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, ...other } = props;
+ const { open, onOpenChange, popupElementId } = useAlertDialogRootContext();
+
+ const { getRootProps } = useDialogTrigger({
+ open,
+ onOpenChange,
+ popupElementId,
+ });
+
+ const ownerState: AlertDialogTriggerOwnerState = { open };
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'button',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ extraProps: other,
+ customStyleHookMapping: {
+ open: (value) => ({ 'data-state': value ? 'open' : 'closed' }),
+ },
+ ref: forwardedRef,
+ });
+
+ return renderElement();
+});
+
+AlertDialogTrigger.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { AlertDialogTrigger };
diff --git a/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.types.ts b/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.types.ts
new file mode 100644
index 0000000000..8ba4f56785
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.types.ts
@@ -0,0 +1,8 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface AlertDialogTriggerProps
+ extends BaseUIComponentProps<'button', AlertDialogTriggerOwnerState> {}
+
+export interface AlertDialogTriggerOwnerState {
+ open: boolean;
+}
diff --git a/packages/mui-base/src/AlertDialog/index.barrel.ts b/packages/mui-base/src/AlertDialog/index.barrel.ts
new file mode 100644
index 0000000000..ad3237aba9
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/index.barrel.ts
@@ -0,0 +1,38 @@
+export { AlertDialogBackdrop } from './Backdrop/AlertDialogBackdrop';
+export type {
+ AlertDialogBackdropProps,
+ AlertDialogBackdropOwnerState,
+} from './Backdrop/AlertDialogBackdrop.types';
+
+export { AlertDialogClose } from './Close/AlertDialogClose';
+export type {
+ AlertDialogCloseProps,
+ AlertDialogCloseOwnerState,
+} from './Close/AlertDialogClose.types';
+
+export { AlertDialogDescription } from './Description/AlertDialogDescription';
+export type {
+ AlertDialogDescriptionProps,
+ AlertDialogDescriptionOwnerState,
+} from './Description/AlertDialogDescription.types';
+
+export { AlertDialogPopup } from './Popup/AlertDialogPopup';
+export type {
+ AlertDialogPopupProps,
+ AlertDialogPopupOwnerState,
+} from './Popup/AlertDialogPopup.types';
+
+export { AlertDialogRoot } from './Root/AlertDialogRoot';
+export type { AlertDialogRootProps } from './Root/AlertDialogRoot.types';
+
+export { AlertDialogTitle } from './Title/AlertDialogTitle';
+export type {
+ AlertDialogTitleProps,
+ AlertDialogTitleOwnerState,
+} from './Title/AlertDialogTitle.types';
+
+export { AlertDialogTrigger } from './Trigger/AlertDialogTrigger';
+export type {
+ AlertDialogTriggerProps,
+ AlertDialogTriggerOwnerState,
+} from './Trigger/AlertDialogTrigger.types';
diff --git a/packages/mui-base/src/AlertDialog/index.ts b/packages/mui-base/src/AlertDialog/index.ts
new file mode 100644
index 0000000000..455525c0a1
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/index.ts
@@ -0,0 +1,38 @@
+export { AlertDialogBackdrop as Backdrop } from './Backdrop/AlertDialogBackdrop';
+export type {
+ AlertDialogBackdropProps as BackdropProps,
+ AlertDialogBackdropOwnerState as BackdropOwnerState,
+} from './Backdrop/AlertDialogBackdrop.types';
+
+export { AlertDialogClose as Close } from './Close/AlertDialogClose';
+export type {
+ AlertDialogCloseProps as CloseProps,
+ AlertDialogCloseOwnerState as CloseOwnerState,
+} from './Close/AlertDialogClose.types';
+
+export { AlertDialogDescription as Description } from './Description/AlertDialogDescription';
+export type {
+ AlertDialogDescriptionProps as DescriptionProps,
+ AlertDialogDescriptionOwnerState as DescriptionOwnerState,
+} from './Description/AlertDialogDescription.types';
+
+export { AlertDialogPopup as Popup } from './Popup/AlertDialogPopup';
+export type {
+ AlertDialogPopupProps as PopupProps,
+ AlertDialogPopupOwnerState as PopupOwnerState,
+} from './Popup/AlertDialogPopup.types';
+
+export { AlertDialogRoot as Root } from './Root/AlertDialogRoot';
+export type { AlertDialogRootProps as RootProps } from './Root/AlertDialogRoot.types';
+
+export { AlertDialogTitle as Title } from './Title/AlertDialogTitle';
+export type {
+ AlertDialogTitleProps as TitleProps,
+ AlertDialogTitleOwnerState as TitleOwnerState,
+} from './Title/AlertDialogTitle.types';
+
+export { AlertDialogTrigger as Trigger } from './Trigger/AlertDialogTrigger';
+export type {
+ AlertDialogTriggerProps as TriggerProps,
+ AlertDialogTriggerOwnerState as TriggerOwnerState,
+} from './Trigger/AlertDialogTrigger.types';
diff --git a/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.test.tsx b/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.test.tsx
new file mode 100644
index 0000000000..b29002bd2e
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.test.tsx
@@ -0,0 +1,31 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Dialog from '@base_ui/react/Dialog';
+import { describeConformance } from '../../../test/describeConformance';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLDivElement,
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+
+ it('has role="presentation"', () => {
+ const { getByTestId } = render(
+
+
+ ,
+ );
+
+ expect(getByTestId('backdrop')).to.have.attribute('role', 'presentation');
+ });
+});
diff --git a/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.tsx b/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.tsx
new file mode 100644
index 0000000000..f9689a7605
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.tsx
@@ -0,0 +1,89 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingPortal } from '@floating-ui/react';
+import type { DialogBackdropOwnerState, DialogBackdropProps } from './DialogBackdrop.types';
+import { useDialogBackdrop } from './useDialogBackdrop';
+import { useDialogRootContext } from '../Root/DialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+
+const DialogBackdrop = React.forwardRef(function DialogBackdrop(
+ props: DialogBackdropProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, keepMounted = false, ...other } = props;
+ const { open, modal, hasParentDialog, setBackdropPresent, animated } = useDialogRootContext();
+
+ const handleMount = React.useCallback(() => setBackdropPresent(true), [setBackdropPresent]);
+ const handleUnmount = React.useCallback(() => setBackdropPresent(false), [setBackdropPresent]);
+
+ const { getRootProps, mounted, transitionStatus } = useDialogBackdrop({
+ animated,
+ open,
+ ref: forwardedRef,
+ onMount: handleMount,
+ onUnmount: handleUnmount,
+ });
+
+ const ownerState: DialogBackdropOwnerState = React.useMemo(
+ () => ({ open, modal, transitionStatus }),
+ [open, modal, transitionStatus],
+ );
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'div',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ extraProps: other,
+ customStyleHookMapping: {
+ open: (value) => ({ 'data-state': value ? 'open' : 'closed' }),
+ transitionStatus: (value) => {
+ if (value === 'entering') {
+ return { 'data-entering': '' } as Record;
+ }
+ if (value === 'exiting') {
+ return { 'data-exiting': '' };
+ }
+ return null;
+ },
+ },
+ });
+
+ if (!mounted && !keepMounted) {
+ return null;
+ }
+
+ if (hasParentDialog) {
+ // no need to render nested backdrops
+ return null;
+ }
+
+ return {renderElement()} ;
+});
+
+DialogBackdrop.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * If `true`, the backdrop element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { DialogBackdrop };
diff --git a/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.types.ts b/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.types.ts
new file mode 100644
index 0000000000..3a95ac0b08
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.types.ts
@@ -0,0 +1,56 @@
+import { TransitionStatus } from '../../utils/useTransitionStatus';
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface DialogBackdropProps extends BaseUIComponentProps<'div', DialogBackdropOwnerState> {
+ /**
+ * If `true`, the backdrop element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted?: boolean;
+}
+
+export interface DialogBackdropOwnerState {
+ open: boolean;
+ modal: boolean;
+ transitionStatus: TransitionStatus;
+}
+
+export interface UseDialogBackdropParams {
+ /**
+ * If `true`, the dialog supports CSS-based animations and transitions.
+ * It is kept in the DOM until the animation completes.
+ */
+ animated: boolean;
+ /**
+ * Determines if the dialog is open.
+ */
+ open: boolean;
+ /**
+ * The ref to the background element.
+ */
+ ref: React.Ref;
+ /**
+ * Callback to invoke when the backdrop is mounted.
+ */
+ onMount: () => void;
+ /**
+ * Callback to invoke when the backdrop is unmounted.
+ */
+ onUnmount: () => void;
+}
+
+export interface UseDialogBackdropReturnValue {
+ /**
+ * Resolver for the root element props.
+ */
+ getRootProps: (externalProps?: Record) => Record;
+ /**
+ * Determines if the dialog should be mounted even if closed (as the exit animation is still in progress).
+ */
+ mounted: boolean;
+ /**
+ * The current transition status of the dialog.
+ */
+ transitionStatus: TransitionStatus;
+}
diff --git a/packages/mui-base/src/Dialog/Backdrop/useDialogBackdrop.ts b/packages/mui-base/src/Dialog/Backdrop/useDialogBackdrop.ts
new file mode 100644
index 0000000000..36fe16375d
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Backdrop/useDialogBackdrop.ts
@@ -0,0 +1,50 @@
+import * as React from 'react';
+import type { UseDialogBackdropParams, UseDialogBackdropReturnValue } from './DialogBackdrop.types';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import { useAnimatedElement } from '../../utils/useAnimatedElement';
+import { useForkRef } from '../../utils/useForkRef';
+import { useEventCallback } from '../../utils/useEventCallback';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
+
+/**
+ *
+ * API:
+ *
+ * - [useDialogBackdrop API](https://mui.com/base-ui/api/use-dialog-backdrop/)
+ */
+export function useDialogBackdrop(params: UseDialogBackdropParams): UseDialogBackdropReturnValue {
+ const { animated, open, ref, onMount: onMountParam, onUnmount: onUnmountParam } = params;
+
+ const backdropRef = React.useRef(null);
+ const handleRef = useForkRef(ref, backdropRef);
+
+ const { mounted, transitionStatus } = useAnimatedElement({
+ open,
+ ref: backdropRef,
+ enabled: animated,
+ });
+
+ const onMount = useEventCallback(onMountParam);
+ const onUnmount = useEventCallback(onUnmountParam);
+
+ useEnhancedEffect(() => {
+ onMount();
+
+ return onUnmount;
+ }, [onMount, onUnmount]);
+
+ const getRootProps = React.useCallback(
+ (externalProps: React.ComponentPropsWithRef) =>
+ mergeReactProps(externalProps, {
+ role: 'presentation',
+ ref: handleRef,
+ }),
+ [handleRef],
+ );
+
+ return {
+ getRootProps,
+ mounted,
+ transitionStatus,
+ };
+}
diff --git a/packages/mui-base/src/Dialog/Close/DialogClose.test.tsx b/packages/mui-base/src/Dialog/Close/DialogClose.test.tsx
new file mode 100644
index 0000000000..6bc1eabc85
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Close/DialogClose.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Dialog from '@base_ui/react/Dialog';
+import { describeConformance } from '../../../test/describeConformance';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLButtonElement,
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/Dialog/Close/DialogClose.tsx b/packages/mui-base/src/Dialog/Close/DialogClose.tsx
new file mode 100644
index 0000000000..19cae98dca
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Close/DialogClose.tsx
@@ -0,0 +1,52 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { DialogCloseProps } from './DialogClose.types';
+import { useDialogClose } from './useDialogClose';
+import { useDialogRootContext } from '../Root/DialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+
+const DialogClose = React.forwardRef(function DialogClose(
+ props: DialogCloseProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, ...other } = props;
+ const { open, onOpenChange, modal } = useDialogRootContext();
+ const { getRootProps } = useDialogClose({ open, onOpenChange });
+
+ const ownerState = {
+ open,
+ modal,
+ };
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'button',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ ref: forwardedRef,
+ extraProps: other,
+ });
+
+ return renderElement();
+});
+
+DialogClose.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { DialogClose };
diff --git a/packages/mui-base/src/Dialog/Close/DialogClose.types.ts b/packages/mui-base/src/Dialog/Close/DialogClose.types.ts
new file mode 100644
index 0000000000..5eff54dbbb
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Close/DialogClose.types.ts
@@ -0,0 +1,26 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface DialogCloseProps extends BaseUIComponentProps<'button', DialogCloseOwnerState> {}
+
+export interface DialogCloseOwnerState {
+ open: boolean;
+ modal: boolean;
+}
+
+export interface UseDialogCloseParameters {
+ /**
+ * Determines whether the dialog is open.
+ */
+ open: boolean;
+ /**
+ * Callback invoked when the dialog is being opened or closed.
+ */
+ onOpenChange: (open: boolean) => void;
+}
+
+export interface UseDialogCloseReturnValue {
+ /**
+ * Resolver for the root element props.
+ */
+ getRootProps: (externalProps: React.HTMLAttributes) => React.HTMLAttributes;
+}
diff --git a/packages/mui-base/src/Dialog/Close/useDialogClose.ts b/packages/mui-base/src/Dialog/Close/useDialogClose.ts
new file mode 100644
index 0000000000..589b4700a6
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Close/useDialogClose.ts
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import type { UseDialogCloseParameters, UseDialogCloseReturnValue } from './DialogClose.types';
+/**
+ *
+ * Demos:
+ *
+ * - [Dialog](https://mui.com/base-ui/react-dialog/#hooks)
+ *
+ * API:
+ *
+ * - [useDialogClose API](https://mui.com/base-ui/react-dialog/hooks-api/#use-dialog-close)
+ */
+export function useDialogClose(params: UseDialogCloseParameters): UseDialogCloseReturnValue {
+ const { open, onOpenChange } = params;
+ const handleClick = React.useCallback(() => {
+ if (open) {
+ onOpenChange?.(false);
+ }
+ }, [open, onOpenChange]);
+
+ const getRootProps = (externalProps: React.HTMLAttributes) =>
+ mergeReactProps(externalProps, { onClick: handleClick });
+
+ return {
+ getRootProps,
+ };
+}
diff --git a/packages/mui-base/src/Dialog/Description/DialogDescription.test.tsx b/packages/mui-base/src/Dialog/Description/DialogDescription.test.tsx
new file mode 100644
index 0000000000..7a9a8df113
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Description/DialogDescription.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Dialog from '@base_ui/react/Dialog';
+import { describeConformance } from '../../../test/describeConformance';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLParagraphElement,
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/Dialog/Description/DialogDescription.tsx b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx
new file mode 100644
index 0000000000..e556681f69
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type { DialogDescriptionProps } from './DialogDescription.types';
+import { useDialogRootContext } from '../Root/DialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
+import { useId } from '../../utils/useId';
+
+const DialogDescription = React.forwardRef(function DialogDescription(
+ props: DialogDescriptionProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, id: idProp, ...other } = props;
+ const { setDescriptionElementId, open, modal } = useDialogRootContext();
+
+ const ownerState = {
+ open,
+ modal,
+ };
+
+ const id = useId(idProp);
+
+ useEnhancedEffect(() => {
+ setDescriptionElementId(id);
+ return () => {
+ setDescriptionElementId(undefined);
+ };
+ }, [id, setDescriptionElementId]);
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'p',
+ className,
+ ownerState,
+ ref: forwardedRef,
+ extraProps: other,
+ });
+
+ return renderElement();
+});
+
+DialogDescription.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * @ignore
+ */
+ id: PropTypes.string,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { DialogDescription };
diff --git a/packages/mui-base/src/Dialog/Description/DialogDescription.types.ts b/packages/mui-base/src/Dialog/Description/DialogDescription.types.ts
new file mode 100644
index 0000000000..9336b958e7
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Description/DialogDescription.types.ts
@@ -0,0 +1,9 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface DialogDescriptionProps
+ extends BaseUIComponentProps<'p', DialogDescriptionOwnerState> {}
+
+export interface DialogDescriptionOwnerState {
+ open: boolean;
+ modal: boolean;
+}
diff --git a/packages/mui-base/src/Dialog/Popup/DialogPopup.test.tsx b/packages/mui-base/src/Dialog/Popup/DialogPopup.test.tsx
new file mode 100644
index 0000000000..901afa4a38
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Popup/DialogPopup.test.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import * as Dialog from '@base_ui/react/Dialog';
+import { describeConformance, createRenderer } from '../../../test';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLDivElement,
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+
+ describe('prop: keepMounted', () => {
+ [
+ [true, true],
+ [false, false],
+ [undefined, false],
+ ].forEach(([keepMounted, expectedIsMounted]) => {
+ it(`should ${!expectedIsMounted ? 'not ' : ''}keep the dialog mounted when keepMounted=${keepMounted}`, async () => {
+ const { queryByRole } = await render(
+
+
+ ,
+ );
+
+ const dialog = queryByRole('dialog', { hidden: true });
+ if (expectedIsMounted) {
+ expect(dialog).not.to.equal(null);
+ expect(dialog).toBeInaccessible();
+ } else {
+ expect(dialog).to.equal(null);
+ }
+ });
+ });
+ });
+});
diff --git a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx
new file mode 100644
index 0000000000..da4212a7af
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx
@@ -0,0 +1,107 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingFocusManager, FloatingPortal } from '@floating-ui/react';
+import { DialogPopupOwnerState, DialogPopupProps } from './DialogPopup.types';
+import { useDialogPopup } from './useDialogPopup';
+import { useDialogRootContext } from '../Root/DialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { refType, HTMLElementType } from '../../utils/proptypes';
+
+const DialogPopup = React.forwardRef(function DialogPopup(
+ props: DialogPopupProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, container, id, keepMounted = false, render, ...other } = props;
+ const rootContext = useDialogRootContext();
+ const { open, modal, nestedOpenDialogCount, dismissible } = rootContext;
+
+ const { getRootProps, floatingContext, mounted, transitionStatus } = useDialogPopup({
+ id,
+ ref: forwardedRef,
+ isTopmost: nestedOpenDialogCount === 0,
+ ...rootContext,
+ });
+
+ const ownerState: DialogPopupOwnerState = {
+ open,
+ modal,
+ nestedOpenDialogCount,
+ transitionStatus,
+ };
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'div',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ extraProps: {
+ ...other,
+ style: { '--nested-dialogs': nestedOpenDialogCount },
+ },
+ customStyleHookMapping: {
+ open: (value) => ({ 'data-state': value ? 'open' : 'closed' }),
+ nestedOpenDialogCount: (value) => ({ 'data-nested-dialogs': value.toString() }),
+ transitionStatus: (value) => {
+ if (value === 'entering') {
+ return { 'data-entering': '' } as Record;
+ }
+ if (value === 'exiting') {
+ return { 'data-exiting': '' };
+ }
+ return null;
+ },
+ },
+ });
+
+ if (!keepMounted && !mounted) {
+ return null;
+ }
+
+ return (
+
+
+ {renderElement()}
+
+
+ );
+});
+
+DialogPopup.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * The container element to which the popup is appended to.
+ */
+ container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, refType]),
+ /**
+ * @ignore
+ */
+ id: PropTypes.string,
+ /**
+ * If `true`, the dialog element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { DialogPopup };
diff --git a/packages/mui-base/src/Dialog/Popup/DialogPopup.types.ts b/packages/mui-base/src/Dialog/Popup/DialogPopup.types.ts
new file mode 100644
index 0000000000..fed361bff7
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Popup/DialogPopup.types.ts
@@ -0,0 +1,94 @@
+import * as React from 'react';
+import { type FloatingContext } from '@floating-ui/react';
+import { type BaseUIComponentProps } from '../../utils/types';
+import { TransitionStatus } from '../../utils/useTransitionStatus';
+
+export interface DialogPopupProps extends BaseUIComponentProps<'div', DialogPopupOwnerState> {
+ /**
+ * The container element to which the popup is appended to.
+ */
+ container?: HTMLElement | null | React.MutableRefObject;
+ /**
+ * If `true`, the dialog element is kept in the DOM when closed.
+ *
+ * @default false
+ */
+ keepMounted?: boolean;
+}
+
+export interface DialogPopupOwnerState {
+ open: boolean;
+ modal: boolean;
+ nestedOpenDialogCount: number;
+ transitionStatus: TransitionStatus;
+}
+
+export interface UseDialogPopupParameters {
+ /**
+ * If `true`, the dialog supports CSS-based animations and transitions.
+ * It is kept in the DOM until the animation completes.
+ */
+ animated: boolean;
+ /**
+ * The id of the dialog element.
+ */
+ id?: string;
+ /**
+ * The ref to the dialog element.
+ */
+ ref: React.Ref;
+ /**
+ * Determines if the dialog is modal.
+ */
+ modal: boolean;
+ /**
+ * Determines if the dialog is open.
+ */
+ open: boolean;
+ /**
+ * Callback fired when the dialog is requested to be opened or closed.
+ */
+ onOpenChange: (open: boolean) => void;
+ /**
+ * The id of the title element associated with the dialog.
+ */
+ titleElementId: string | undefined;
+ /**
+ * The id of the description element associated with the dialog.
+ */
+ descriptionElementId: string | undefined;
+ /**
+ * Callback to set the id of the popup element.
+ */
+ setPopupElementId: (id: string | undefined) => void;
+ /**
+ * Determines whether the dialog should close when clicking outside of it.
+ * @default true
+ */
+ dismissible?: boolean;
+ /**
+ * Determines if the dialog is the top-most one.
+ */
+ isTopmost: boolean;
+}
+
+export interface UseDialogPopupReturnValue {
+ /**
+ * Floating UI context for the dialog's FloatingFocusManager.
+ */
+ floatingContext: FloatingContext;
+ /**
+ * Resolver for the root element props.
+ */
+ getRootProps: (
+ externalProps: React.ComponentPropsWithRef<'div'>,
+ ) => React.ComponentPropsWithRef<'div'>;
+ /**
+ * Determines if the dialog should be mounted even if closed (as the exit animation is still in progress).
+ */
+ mounted: boolean;
+ /**
+ * The current transition status of the dialog.
+ */
+ transitionStatus: TransitionStatus;
+}
diff --git a/packages/mui-base/src/Dialog/Popup/useDialogPopup.tsx b/packages/mui-base/src/Dialog/Popup/useDialogPopup.tsx
new file mode 100644
index 0000000000..e629a38615
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Popup/useDialogPopup.tsx
@@ -0,0 +1,87 @@
+import * as React from 'react';
+import { useFloating, useInteractions, useDismiss } from '@floating-ui/react';
+import { UseDialogPopupParameters, UseDialogPopupReturnValue } from './DialogPopup.types';
+import { useId } from '../../utils/useId';
+import { useForkRef } from '../../utils/useForkRef';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import { useAnimatedElement } from '../../utils/useAnimatedElement';
+import { useScrollLock } from '../../utils/useScrollLock';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
+
+/**
+ *
+ * Demos:
+ *
+ * - [Dialog](https://mui.com/base-ui/react-dialog/#hooks)
+ *
+ * API:
+ *
+ * - [useDialogPopup API](https://mui.com/base-ui/react-dialog/hooks-api/#use-dialog-popup)
+ */
+export function useDialogPopup(parameters: UseDialogPopupParameters): UseDialogPopupReturnValue {
+ const {
+ animated,
+ descriptionElementId,
+ id: idParam,
+ modal,
+ onOpenChange,
+ open,
+ ref,
+ setPopupElementId,
+ dismissible,
+ titleElementId,
+ isTopmost,
+ } = parameters;
+
+ const { refs, context } = useFloating({
+ open,
+ onOpenChange,
+ });
+
+ const popupRef = React.useRef(null);
+
+ const dismiss = useDismiss(context, {
+ outsidePressEvent: 'mousedown',
+ outsidePress: isTopmost && dismissible,
+ escapeKey: isTopmost,
+ });
+ const { getFloatingProps } = useInteractions([dismiss]);
+
+ const id = useId(idParam);
+ const handleRef = useForkRef(ref, popupRef, refs.setFloating);
+
+ const { mounted, transitionStatus } = useAnimatedElement({
+ open,
+ ref: popupRef,
+ enabled: animated,
+ });
+
+ useScrollLock(modal && mounted);
+
+ useEnhancedEffect(() => {
+ setPopupElementId(id);
+ return () => {
+ setPopupElementId(undefined);
+ };
+ }, [id, setPopupElementId]);
+
+ const getRootProps = (externalProps: React.HTMLAttributes) =>
+ mergeReactProps(externalProps, {
+ 'aria-labelledby': titleElementId ?? undefined,
+ 'aria-describedby': descriptionElementId ?? undefined,
+ 'aria-hidden': !open || undefined,
+ 'aria-modal': open && modal ? true : undefined,
+ role: 'dialog',
+ tabIndex: -1,
+ ...getFloatingProps(),
+ id,
+ ref: handleRef,
+ });
+
+ return {
+ floatingContext: context,
+ getRootProps,
+ mounted,
+ transitionStatus,
+ };
+}
diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.test.tsx b/packages/mui-base/src/Dialog/Root/DialogRoot.test.tsx
new file mode 100644
index 0000000000..d31cc84e2a
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Root/DialogRoot.test.tsx
@@ -0,0 +1,211 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { spy } from 'sinon';
+import { act, fireEvent } from '@mui/internal-test-utils';
+import * as Dialog from '@base_ui/react/Dialog';
+import { createRenderer } from '../../../test';
+
+async function wait(timeout: number) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, timeout);
+ });
+}
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describe('uncontrolled mode', () => {
+ it('should open the dialog with the trigger', async () => {
+ const { queryByRole, getByRole } = await render(
+
+
+
+ ,
+ );
+
+ const button = getByRole('button');
+ expect(queryByRole('dialog')).to.equal(null);
+
+ act(() => {
+ button.click();
+ });
+
+ expect(queryByRole('dialog')).not.to.equal(null);
+ });
+ });
+
+ describe('controlled mode', () => {
+ it('should open and close the dialog with the `open` prop', async () => {
+ const { queryByRole, setProps } = await render(
+
+
+ ,
+ );
+
+ expect(queryByRole('dialog')).to.equal(null);
+
+ setProps({ open: true });
+ expect(queryByRole('dialog')).not.to.equal(null);
+
+ setProps({ open: false });
+ expect(queryByRole('dialog')).to.equal(null);
+ });
+ });
+
+ describe('prop: modal', () => {
+ it('warns when the dialog is modal but no backdrop is present', async () => {
+ await expect(() =>
+ render(
+
+
+ ,
+ ),
+ ).toWarnDev([
+ 'Base UI: The Dialog is modal but no backdrop is present. Add the backdrop component to prevent interacting with the rest of the page.',
+ 'Base UI: The Dialog is modal but no backdrop is present. Add the backdrop component to prevent interacting with the rest of the page.',
+ ]);
+ });
+
+ it('does not warn when the dialog is not modal and no backdrop is present', () => {
+ expect(() =>
+ render(
+
+
+ ,
+ ),
+ ).not.toWarnDev();
+ });
+
+ it('does not warn when the dialog is modal and backdrop is present', () => {
+ expect(() =>
+ render(
+
+
+
+ ,
+ ),
+ ).not.toWarnDev();
+ });
+ });
+
+ describe('prop: dismissible', () => {
+ (
+ [
+ [true, true],
+ [false, false],
+ [undefined, true],
+ ] as const
+ ).forEach(([dismissible, expectDismissed]) => {
+ it(`${expectDismissed ? 'closes' : 'does not close'} the dialog when clicking outside if dismissible=${dismissible}`, async () => {
+ const handleOpenChange = spy();
+
+ const { getByTestId, queryByRole } = await render(
+
+
+
+
+
,
+ );
+
+ const outside = getByTestId('outside');
+
+ fireEvent.mouseDown(outside);
+ expect(handleOpenChange.calledOnce).to.equal(expectDismissed);
+
+ if (expectDismissed) {
+ expect(queryByRole('dialog')).to.equal(null);
+ } else {
+ expect(queryByRole('dialog')).not.to.equal(null);
+ }
+ });
+ });
+ });
+
+ describe('prop: animated', () => {
+ before(function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+ });
+
+ const css = `
+ .dialog {
+ opacity: 0;
+ transition: opacity 200ms;
+ }
+
+ .dialog[data-state='open'] {
+ opacity: 1;
+ }
+ `;
+
+ it('when `true`, waits for the exit transition to finish before unmounting', async () => {
+ const { setProps, queryByRole } = await render(
+
+ {/* eslint-disable-next-line react/no-danger */}
+
+
+ ,
+ );
+
+ setProps({ open: false });
+ expect(queryByRole('dialog', { hidden: true })).not.to.equal(null);
+ });
+
+ it('when `false`, unmounts the popup immediately', async () => {
+ const { setProps, queryByRole } = await render(
+
+ {/* eslint-disable-next-line react/no-danger */}
+
+
+ ,
+ );
+
+ setProps({ open: false });
+ expect(queryByRole('dialog')).to.equal(null);
+ });
+ });
+
+ describe('focus management', () => {
+ before(function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+ });
+
+ it('should focus the first focusable element within the popup', async () => {
+ const { getByText, getByTestId } = await render(
+
+
+
+ Open
+
+
+ Close
+
+
+
+
,
+ );
+
+ const trigger = getByText('Open');
+ act(() => {
+ trigger.click();
+ });
+
+ // wait for the focus to be settled (takes some time on CI)
+ await wait(50);
+
+ const dialogInput = getByTestId('dialog-input');
+ expect(dialogInput).to.toHaveFocus();
+ });
+ });
+});
diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx
new file mode 100644
index 0000000000..9798f86c41
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { DialogRootProps } from './DialogRoot.types';
+import { DialogRootContext } from './DialogRootContext';
+import { useDialogRoot } from './useDialogRoot';
+
+const DialogRoot = function DialogRoot(props: DialogRootProps) {
+ const {
+ children,
+ defaultOpen,
+ modal = true,
+ onOpenChange,
+ open: openProp,
+ dismissible = true,
+ animated = true,
+ } = props;
+
+ const dialogRootContext = React.useContext(DialogRootContext);
+
+ const dialogRoot = useDialogRoot({
+ open: openProp,
+ defaultOpen,
+ onOpenChange,
+ modal,
+ dismissible,
+ onNestedDialogClose: dialogRootContext?.onNestedDialogClose,
+ onNestedDialogOpen: dialogRootContext?.onNestedDialogOpen,
+ });
+
+ const hasParentDialog = Boolean(dialogRootContext);
+
+ const contextValue = React.useMemo(
+ () => ({ ...dialogRoot, hasParentDialog, dismissible, animated }),
+ [dialogRoot, hasParentDialog, dismissible, animated],
+ );
+
+ return {children} ;
+};
+
+DialogRoot.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * If `true`, the dialog supports CSS-based animations and transitions.
+ * It is kept in the DOM until the animation completes.
+ *
+ * @default true
+ */
+ animated: PropTypes.bool,
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Determines whether the dialog is initally open.
+ * This is an uncontrolled equivalent of the `open` prop.
+ */
+ defaultOpen: PropTypes.bool,
+ /**
+ * Determines whether the dialog should close when clicking outside of it.
+ * @default true
+ */
+ dismissible: PropTypes.bool,
+ /**
+ * Determines whether the dialog is modal.
+ * @default true
+ */
+ modal: PropTypes.bool,
+ /**
+ * Callback invoked when the dialog is being opened or closed.
+ */
+ onOpenChange: PropTypes.func,
+ /**
+ * Determines whether the dialog is open.
+ */
+ open: PropTypes.bool,
+} as any;
+
+export { DialogRoot };
diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts
new file mode 100644
index 0000000000..667127bb2c
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts
@@ -0,0 +1,119 @@
+interface DialogRootParameters {
+ /**
+ * If `true`, the dialog supports CSS-based animations and transitions.
+ * It is kept in the DOM until the animation completes.
+ *
+ * @default true
+ */
+ animated?: boolean;
+ /**
+ * Determines whether the dialog is open.
+ */
+ open?: boolean;
+ /**
+ * Determines whether the dialog is initally open.
+ * This is an uncontrolled equivalent of the `open` prop.
+ */
+ defaultOpen?: boolean;
+ /**
+ * Determines whether the dialog is modal.
+ * @default true
+ */
+ modal?: boolean;
+ /**
+ * Callback invoked when the dialog is being opened or closed.
+ */
+ onOpenChange?: (open: boolean) => void;
+ /**
+ * Determines whether the dialog should close when clicking outside of it.
+ * @default true
+ */
+ dismissible?: boolean;
+}
+
+export interface DialogRootProps extends DialogRootParameters {
+ children?: React.ReactNode;
+}
+
+export interface UseDialogRootParameters extends DialogRootParameters {
+ /**
+ * Callback to invoke when a nested dialog is opened.
+ */
+ onNestedDialogOpen?: (ownChildrenCount: number) => void;
+ /**
+ * Callback to invoke when a nested dialog is closed.
+ */
+ onNestedDialogClose?: () => void;
+}
+
+export interface UseDialogRootReturnValue {
+ /**
+ * The id of the description element associated with the dialog.
+ */
+ descriptionElementId: string | undefined;
+ /**
+ * Determines if the dialog is modal.
+ */
+ modal: boolean;
+ /**
+ * Number of nested dialogs that are currently open.
+ */
+ nestedOpenDialogCount: number;
+ /**
+ * Callback to invoke when a nested dialog is closed.
+ */
+ onNestedDialogClose?: () => void;
+ /**
+ * Callback to invoke when a nested dialog is opened.
+ */
+ onNestedDialogOpen?: (ownChildrenCount: number) => void;
+ /**
+ * Callback to fire when the dialog is requested to be opened or closed.
+ */
+ onOpenChange: (open: boolean) => void;
+ /**
+ * Determines if the dialog is open.
+ */
+ open: boolean;
+ /**
+ * The id of the popup element.
+ */
+ popupElementId: string | undefined;
+ /**
+ * Callback to notify the dialog that the backdrop is present.
+ */
+ setBackdropPresent: (present: boolean) => void;
+ /**
+ * Callback to set the id of the description element associated with the dialog.
+ */
+ setDescriptionElementId: (elementId: string | undefined) => void;
+ /**
+ * Callback to set the id of the popup element.
+ */
+ setPopupElementId: (elementId: string | undefined) => void;
+ /**
+ * Callback to set the id of the title element.
+ */
+ setTitleElementId: (elementId: string | undefined) => void;
+ /**
+ * The id of the title element associated with the dialog.
+ */
+ titleElementId: string | undefined;
+}
+
+export interface DialogRootContextValue extends UseDialogRootReturnValue {
+ /**
+ * If `true`, the dialog supports CSS-based animations and transitions.
+ * It is kept in the DOM until the animation completes.
+ */
+ animated: boolean;
+ /**
+ * Determines if the dialog is nested within a parent dialog.
+ */
+ hasParentDialog: boolean;
+ /**
+ * Determines whether the dialog should close when clicking outside of it.
+ * @default true
+ */
+ dismissible?: boolean;
+}
diff --git a/packages/mui-base/src/Dialog/Root/DialogRootContext.ts b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts
new file mode 100644
index 0000000000..7b24b62107
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import type { DialogRootContextValue } from './DialogRoot.types';
+
+export const DialogRootContext = React.createContext(undefined);
+
+export function useDialogRootContext() {
+ const context = React.useContext(DialogRootContext);
+ if (context === undefined) {
+ throw new Error('useDialogRootContext must be used within a DialogRoot');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/Dialog/Root/useDialogRoot.ts b/packages/mui-base/src/Dialog/Root/useDialogRoot.ts
new file mode 100644
index 0000000000..ce8c094932
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Root/useDialogRoot.ts
@@ -0,0 +1,119 @@
+import * as React from 'react';
+import type { UseDialogRootParameters, UseDialogRootReturnValue } from './DialogRoot.types';
+import { useControlled } from '../../utils/useControlled';
+
+/**
+ *
+ * Demos:
+ *
+ * - [Dialog](https://mui.com/base-ui/react-dialog/#hooks)
+ *
+ * API:
+ *
+ * - [useDialogRoot API](https://mui.com/base-ui/react-dialog/hooks-api/#use-dialog-root)
+ */
+export function useDialogRoot(parameters: UseDialogRootParameters): UseDialogRootReturnValue {
+ const {
+ open: openParam,
+ defaultOpen = false,
+ onOpenChange,
+ modal = true,
+ onNestedDialogOpen,
+ onNestedDialogClose,
+ } = parameters;
+
+ const [open, setOpen] = useControlled({
+ controlled: openParam,
+ default: defaultOpen,
+ name: 'DialogRoot',
+ });
+
+ const [titleElementId, setTitleElementId] = React.useState(undefined);
+ const [descriptionElementId, setDescriptionElementId] = React.useState(
+ undefined,
+ );
+ const [popupElementId, setPopupElementId] = React.useState(undefined);
+ const hasBackdrop = React.useRef(false);
+
+ if (process.env.NODE_ENV !== 'production') {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ React.useEffect(() => {
+ if (modal && !hasBackdrop.current) {
+ console.warn(
+ 'Base UI: The Dialog is modal but no backdrop is present. Add the backdrop component to prevent interacting with the rest of the page.',
+ );
+ }
+ }, [modal]);
+ }
+
+ const handleOpenChange = React.useCallback(
+ (shouldOpen: boolean) => {
+ setOpen(shouldOpen);
+ onOpenChange?.(shouldOpen);
+ },
+ [onOpenChange, setOpen],
+ );
+
+ const [ownNestedOpenDialogs, setOwnNestedOpenDialogs] = React.useState(0);
+
+ React.useEffect(() => {
+ if (onNestedDialogOpen && open) {
+ onNestedDialogOpen(ownNestedOpenDialogs);
+ }
+
+ if (onNestedDialogClose && !open) {
+ onNestedDialogClose();
+ }
+
+ return () => {
+ if (onNestedDialogClose && open) {
+ onNestedDialogClose();
+ }
+
+ if (onNestedDialogOpen && !open) {
+ onNestedDialogOpen(ownNestedOpenDialogs);
+ }
+ };
+ }, [open, onNestedDialogClose, onNestedDialogOpen, ownNestedOpenDialogs]);
+
+ const handleNestedDialogOpen = React.useCallback((ownChildrenCount: number) => {
+ setOwnNestedOpenDialogs(ownChildrenCount + 1);
+ }, []);
+
+ const handleNestedDialogClose = React.useCallback(() => {
+ setOwnNestedOpenDialogs(0);
+ }, []);
+
+ const setBackdropPresent = React.useCallback((present: boolean) => {
+ hasBackdrop.current = present;
+ }, []);
+
+ return React.useMemo(() => {
+ return {
+ modal,
+ onOpenChange: handleOpenChange,
+ open,
+ titleElementId,
+ setTitleElementId,
+ descriptionElementId,
+ setDescriptionElementId,
+ popupElementId,
+ setPopupElementId,
+ onNestedDialogOpen: handleNestedDialogOpen,
+ onNestedDialogClose: handleNestedDialogClose,
+ nestedOpenDialogCount: ownNestedOpenDialogs,
+ setBackdropPresent,
+ };
+ }, [
+ modal,
+ handleOpenChange,
+ open,
+ titleElementId,
+ descriptionElementId,
+ popupElementId,
+ handleNestedDialogClose,
+ handleNestedDialogOpen,
+ ownNestedOpenDialogs,
+ setBackdropPresent,
+ ]);
+}
diff --git a/packages/mui-base/src/Dialog/Title/DialogTitle.test.tsx b/packages/mui-base/src/Dialog/Title/DialogTitle.test.tsx
new file mode 100644
index 0000000000..6470c88d4b
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Title/DialogTitle.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Dialog from '@base_ui/react/Dialog';
+import { describeConformance } from '../../../test/describeConformance';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLHeadingElement,
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/Dialog/Title/DialogTitle.tsx b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx
new file mode 100644
index 0000000000..c6714698bc
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type { DialogTitleProps } from './DialogTitle.types';
+import { useDialogRootContext } from '../Root/DialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
+import { useId } from '../../utils/useId';
+
+const DialogTitle = React.forwardRef(function DialogTitle(
+ props: DialogTitleProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, id: idProp, ...other } = props;
+ const { setTitleElementId, open, modal } = useDialogRootContext();
+
+ const ownerState = {
+ open,
+ modal,
+ };
+
+ const id = useId(idProp);
+
+ useEnhancedEffect(() => {
+ setTitleElementId(id);
+ return () => {
+ setTitleElementId(undefined);
+ };
+ }, [id, setTitleElementId]);
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'h2',
+ className,
+ ownerState,
+ ref: forwardedRef,
+ extraProps: other,
+ });
+
+ return renderElement();
+});
+
+DialogTitle.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * @ignore
+ */
+ id: PropTypes.string,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { DialogTitle };
diff --git a/packages/mui-base/src/Dialog/Title/DialogTitle.types.ts b/packages/mui-base/src/Dialog/Title/DialogTitle.types.ts
new file mode 100644
index 0000000000..443a1afdbb
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Title/DialogTitle.types.ts
@@ -0,0 +1,8 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface DialogTitleProps extends BaseUIComponentProps<'h2', DialogTitleOwnerState> {}
+
+export interface DialogTitleOwnerState {
+ open: boolean;
+ modal: boolean;
+}
diff --git a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.test.tsx b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.test.tsx
new file mode 100644
index 0000000000..3ed1cc81e5
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.test.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Dialog from '@base_ui/react/Dialog';
+import { describeConformance } from '../../../test/describeConformance';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ refInstanceof: window.HTMLButtonElement,
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+});
diff --git a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx
new file mode 100644
index 0000000000..d91c6364a1
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useDialogTrigger } from './useDialogTrigger';
+import type { DialogTriggerOwnerState, DialogTriggerProps } from './DialogTrigger.types';
+import { useDialogRootContext } from '../Root/DialogRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+
+const DialogTrigger = React.forwardRef(function DialogTrigger(
+ props: DialogTriggerProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, ...other } = props;
+ const { open, onOpenChange, modal, popupElementId } = useDialogRootContext();
+
+ const { getRootProps } = useDialogTrigger({
+ open,
+ onOpenChange,
+ popupElementId,
+ });
+
+ const ownerState: DialogTriggerOwnerState = React.useMemo(() => ({ open, modal }), [open, modal]);
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'button',
+ className,
+ ownerState,
+ propGetter: getRootProps,
+ extraProps: other,
+ customStyleHookMapping: {
+ open: (value) => ({ 'data-state': value ? 'open' : 'closed' }),
+ },
+ ref: forwardedRef,
+ });
+
+ return renderElement();
+});
+
+DialogTrigger.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { DialogTrigger };
diff --git a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.types.ts b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.types.ts
new file mode 100644
index 0000000000..feaca73f0f
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.types.ts
@@ -0,0 +1,31 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export interface DialogTriggerProps
+ extends BaseUIComponentProps<'button', DialogTriggerOwnerState> {}
+
+export interface DialogTriggerOwnerState {
+ open: boolean;
+ modal: boolean;
+}
+
+export interface UseDialogTriggerParameters {
+ /**
+ * Determines if the dialog is open.
+ */
+ open: boolean;
+ /**
+ * Callback to fire when the dialog is requested to be opened or closed.
+ */
+ onOpenChange: (open: boolean) => void;
+ /**
+ * The id of the popup element.
+ */
+ popupElementId: string | undefined;
+}
+
+export interface UseDialogTriggerReturnValue {
+ /**
+ * Resolver for the root element props.
+ */
+ getRootProps: (externalProps?: React.HTMLAttributes) => React.HTMLAttributes;
+}
diff --git a/packages/mui-base/src/Dialog/Trigger/useDialogTrigger.ts b/packages/mui-base/src/Dialog/Trigger/useDialogTrigger.ts
new file mode 100644
index 0000000000..e20ebc9e61
--- /dev/null
+++ b/packages/mui-base/src/Dialog/Trigger/useDialogTrigger.ts
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import type {
+ UseDialogTriggerParameters,
+ UseDialogTriggerReturnValue,
+} from './DialogTrigger.types';
+
+/**
+ *
+ * Demos:
+ *
+ * - [Dialog](https://mui.com/base-ui/react-dialog/#hooks)
+ *
+ * API:
+ *
+ * - [useDialogTrigger API](https://mui.com/base-ui/react-dialog/hooks-api/#use-dialog-trigger)
+ */
+export function useDialogTrigger(params: UseDialogTriggerParameters): UseDialogTriggerReturnValue {
+ const { open, onOpenChange, popupElementId } = params;
+
+ const getRootProps = React.useCallback(
+ (externalProps: React.HTMLAttributes = {}) =>
+ mergeReactProps(externalProps, {
+ onClick: () => {
+ if (!open) {
+ onOpenChange?.(true);
+ }
+ },
+ 'aria-haspopup': 'dialog',
+ 'aria-controls': popupElementId ?? undefined,
+ }),
+ [open, onOpenChange, popupElementId],
+ );
+
+ return React.useMemo(
+ () => ({
+ getRootProps,
+ }),
+ [getRootProps],
+ );
+}
diff --git a/packages/mui-base/src/Dialog/index.barrel.ts b/packages/mui-base/src/Dialog/index.barrel.ts
new file mode 100644
index 0000000000..4118fe2146
--- /dev/null
+++ b/packages/mui-base/src/Dialog/index.barrel.ts
@@ -0,0 +1,48 @@
+export { DialogBackdrop } from './Backdrop/DialogBackdrop';
+export type {
+ DialogBackdropProps,
+ DialogBackdropOwnerState,
+} from './Backdrop/DialogBackdrop.types';
+
+export { DialogClose } from './Close/DialogClose';
+export { useDialogClose } from './Close/useDialogClose';
+export type {
+ DialogCloseProps,
+ DialogCloseOwnerState,
+ UseDialogCloseParameters,
+ UseDialogCloseReturnValue,
+} from './Close/DialogClose.types';
+
+export { DialogDescription } from './Description/DialogDescription';
+export type {
+ DialogDescriptionProps,
+ DialogDescriptionOwnerState,
+} from './Description/DialogDescription.types';
+
+export { DialogPopup } from './Popup/DialogPopup';
+export { useDialogPopup } from './Popup/useDialogPopup';
+export type {
+ DialogPopupProps,
+ DialogPopupOwnerState,
+ UseDialogPopupParameters,
+ UseDialogPopupReturnValue,
+} from './Popup/DialogPopup.types';
+
+export { DialogRoot } from './Root/DialogRoot';
+export { useDialogRoot } from './Root/useDialogRoot';
+export type {
+ DialogRootProps,
+ UseDialogRootParameters,
+ UseDialogRootReturnValue,
+} from './Root/DialogRoot.types';
+
+export { DialogTitle } from './Title/DialogTitle';
+export type { DialogTitleProps, DialogTitleOwnerState } from './Title/DialogTitle.types';
+
+export { DialogTrigger } from './Trigger/DialogTrigger';
+export { useDialogTrigger } from './Trigger/useDialogTrigger';
+export type {
+ DialogTriggerProps,
+ UseDialogTriggerParameters,
+ UseDialogTriggerReturnValue,
+} from './Trigger/DialogTrigger.types';
diff --git a/packages/mui-base/src/Dialog/index.ts b/packages/mui-base/src/Dialog/index.ts
new file mode 100644
index 0000000000..8fea472a28
--- /dev/null
+++ b/packages/mui-base/src/Dialog/index.ts
@@ -0,0 +1,52 @@
+export { DialogBackdrop as Backdrop } from './Backdrop/DialogBackdrop';
+export type {
+ DialogBackdropProps as BackdropProps,
+ DialogBackdropOwnerState as BackdropOwnerState,
+} from './Backdrop/DialogBackdrop.types';
+
+export { DialogClose as Close } from './Close/DialogClose';
+export { useDialogClose } from './Close/useDialogClose';
+export type {
+ DialogCloseProps as CloseProps,
+ DialogCloseOwnerState as CloseOwnerState,
+ UseDialogCloseParameters,
+ UseDialogCloseReturnValue,
+} from './Close/DialogClose.types';
+
+export { DialogDescription as Description } from './Description/DialogDescription';
+export type {
+ DialogDescriptionProps as DescriptionProps,
+ DialogDescriptionOwnerState as DescriptionOwnerState,
+} from './Description/DialogDescription.types';
+
+export { DialogPopup as Popup } from './Popup/DialogPopup';
+export { useDialogPopup } from './Popup/useDialogPopup';
+export type {
+ DialogPopupProps as PopupProps,
+ DialogPopupOwnerState as PopupOwnerState,
+ UseDialogPopupParameters,
+ UseDialogPopupReturnValue,
+} from './Popup/DialogPopup.types';
+
+export { DialogRoot as Root } from './Root/DialogRoot';
+export { useDialogRoot } from './Root/useDialogRoot';
+export type {
+ DialogRootProps as RootProps,
+ UseDialogRootParameters,
+ UseDialogRootReturnValue,
+} from './Root/DialogRoot.types';
+
+export { DialogTitle as Title } from './Title/DialogTitle';
+export type {
+ DialogTitleProps as TitleProps,
+ DialogTitleOwnerState as TitleOwnerState,
+} from './Title/DialogTitle.types';
+
+export { DialogTrigger as Trigger } from './Trigger/DialogTrigger';
+export { useDialogTrigger } from './Trigger/useDialogTrigger';
+export type {
+ DialogTriggerProps as TriggerProps,
+ DialogTriggerOwnerState as TriggerOwnerState,
+ UseDialogTriggerParameters,
+ UseDialogTriggerReturnValue,
+} from './Trigger/DialogTrigger.types';
diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts
index 01576785c2..cd6ac4195d 100644
--- a/packages/mui-base/src/index.ts
+++ b/packages/mui-base/src/index.ts
@@ -1,4 +1,6 @@
+export * from './AlertDialog/index.barrel';
export * from './Checkbox/index.barrel';
+export * from './Dialog/index.barrel';
export * from './NumberField/index.barrel';
export * from './Switch/index.barrel';
export * from './Tabs/index.barrel';
diff --git a/packages/mui-base/src/utils/index.ts b/packages/mui-base/src/utils/index.ts
index 155f7d2b38..579f5c2262 100644
--- a/packages/mui-base/src/utils/index.ts
+++ b/packages/mui-base/src/utils/index.ts
@@ -2,5 +2,6 @@
export * from './prepareForSlot';
export * from './MuiCancellableEvent';
-export * from './visuallyHidden';
+export * from './useScrollLock';
export * from './useTransitionStatus';
+export * from './visuallyHidden';
diff --git a/packages/mui-base/src/utils/proptypes.ts b/packages/mui-base/src/utils/proptypes.ts
index b6fa3f4983..c71ada508d 100644
--- a/packages/mui-base/src/utils/proptypes.ts
+++ b/packages/mui-base/src/utils/proptypes.ts
@@ -1,2 +1,2 @@
export { default as refType } from '@mui/utils/refType';
-export { HTMLElementType } from '@mui/utils';
+export { default as HTMLElementType } from '@mui/utils/HTMLElementType';
diff --git a/packages/mui-base/src/utils/useAnimatedElement.ts b/packages/mui-base/src/utils/useAnimatedElement.ts
new file mode 100644
index 0000000000..2aa85bbbae
--- /dev/null
+++ b/packages/mui-base/src/utils/useAnimatedElement.ts
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { useAnimationsFinished } from './useAnimationsFinished';
+import { useTransitionStatus } from './useTransitionStatus';
+
+interface UseAnimatedElementParameters {
+ open: boolean;
+ ref: React.RefObject;
+ enabled: boolean;
+}
+/**
+ * @ignore - internal hook.
+ */
+export function useAnimatedElement(params: UseAnimatedElementParameters) {
+ const { open, ref, enabled } = params;
+
+ const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, enabled);
+ const runOnceAnimationsFinish = useAnimationsFinished(() => ref.current);
+
+ React.useEffect(() => {
+ if (!open) {
+ if (enabled) {
+ runOnceAnimationsFinish(() => {
+ setMounted(false);
+ });
+ } else {
+ setMounted(false);
+ }
+ }
+ }, [enabled, open, runOnceAnimationsFinish, setMounted]);
+
+ return {
+ mounted,
+ transitionStatus,
+ };
+}
diff --git a/packages/mui-base/src/utils/useScrollLock.ts b/packages/mui-base/src/utils/useScrollLock.ts
new file mode 100644
index 0000000000..8f270c8c35
--- /dev/null
+++ b/packages/mui-base/src/utils/useScrollLock.ts
@@ -0,0 +1,79 @@
+import { useEnhancedEffect } from './useEnhancedEffect';
+import { useId } from './useId';
+import { isIOS } from './detectBrowser';
+
+const activeLocks = new Set();
+
+/**
+ * Locks the scroll of the document when enabled.
+ *
+ * API:
+ *
+ * - [useScrollLock API](https://mui.com/base-ui/api/use-scroll-lock/)
+ *
+ * @param enabled - Whether to enable the scroll lock.
+ */
+export function useScrollLock(enabled: boolean = true) {
+ // Based on Floating UI's FloatingOverlay
+
+ const lockId = useId();
+ useEnhancedEffect(() => {
+ if (!enabled) {
+ return undefined;
+ }
+
+ activeLocks.add(lockId!);
+
+ const rootStyle = document.documentElement.style;
+ // RTL scrollbar
+ const scrollbarX =
+ Math.round(document.documentElement.getBoundingClientRect().left) +
+ document.documentElement.scrollLeft;
+ const paddingProp = scrollbarX ? 'paddingLeft' : 'paddingRight';
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+ const scrollX = rootStyle.left ? parseFloat(rootStyle.left) : window.scrollX;
+ const scrollY = rootStyle.top ? parseFloat(rootStyle.top) : window.scrollY;
+
+ rootStyle.overflow = 'hidden';
+
+ if (scrollbarWidth) {
+ rootStyle[paddingProp] = `${scrollbarWidth}px`;
+ }
+
+ // Only iOS doesn't respect `overflow: hidden` on document.body, and this
+ // technique has fewer side effects.
+ if (isIOS()) {
+ // iOS 12 does not support `visualViewport`.
+ const offsetLeft = window.visualViewport?.offsetLeft || 0;
+ const offsetTop = window.visualViewport?.offsetTop || 0;
+
+ Object.assign(rootStyle, {
+ position: 'fixed',
+ top: `${-(scrollY - Math.floor(offsetTop))}px`,
+ left: `${-(scrollX - Math.floor(offsetLeft))}px`,
+ right: '0',
+ });
+ }
+
+ return () => {
+ activeLocks.delete(lockId!);
+
+ if (activeLocks.size === 0) {
+ Object.assign(rootStyle, {
+ overflow: '',
+ [paddingProp]: '',
+ });
+
+ if (isIOS()) {
+ Object.assign(rootStyle, {
+ position: '',
+ top: '',
+ left: '',
+ right: '',
+ });
+ window.scrollTo(scrollX, scrollY);
+ }
+ }
+ };
+ }, [lockId, enabled]);
+}
diff --git a/packages/mui-base/test/conformanceTests/renderProp.tsx b/packages/mui-base/test/conformanceTests/renderProp.tsx
index aaa53f9026..31c3bf63a5 100644
--- a/packages/mui-base/test/conformanceTests/renderProp.tsx
+++ b/packages/mui-base/test/conformanceTests/renderProp.tsx
@@ -80,7 +80,7 @@ export function testRenderProp(
expect(instanceFromRef!).to.have.attribute('data-testid', 'wrapped');
});
- it('should merge the rendering element ref with the custom component ref', () => {
+ it('should merge the rendering element ref with the custom component ref', async () => {
let refA = null;
let refB = null;
@@ -100,9 +100,12 @@ export function testRenderProp(
});
}
- render( );
+ await render( );
+
+ expect(refA).not.to.equal(null);
expect(refA!.tagName).to.equal(Element.toUpperCase());
expect(refA!).to.have.attribute('data-testid', 'wrapped');
+ expect(refB).not.to.equal(null);
expect(refB!.tagName).to.equal(Element.toUpperCase());
expect(refB!).to.have.attribute('data-testid', 'wrapped');
});
diff --git a/packages/mui-base/test/createRenderer.ts b/packages/mui-base/test/createRenderer.ts
new file mode 100644
index 0000000000..6cc0e481d6
--- /dev/null
+++ b/packages/mui-base/test/createRenderer.ts
@@ -0,0 +1,32 @@
+import {
+ CreateRendererOptions,
+ RenderOptions,
+ createRenderer as sharedCreateRenderer,
+ waitFor,
+ act,
+ MuiRenderResult,
+} from '@mui/internal-test-utils';
+
+export function createRenderer(globalOptions?: CreateRendererOptions) {
+ const createRendererResult = sharedCreateRenderer(globalOptions);
+ const { render: originalRender } = createRendererResult;
+
+ const render = async (element: React.ReactElement, options?: RenderOptions) => {
+ let result: MuiRenderResult;
+
+ if (navigator.userAgent.includes('jsdom')) {
+ result = await act(() => originalRender(element, options));
+ } else {
+ result = await originalRender(element, options);
+ }
+
+ // flush microtasks
+ await waitFor(async () => {});
+ return result;
+ };
+
+ return {
+ ...createRendererResult,
+ render,
+ };
+}
diff --git a/packages/mui-base/test/index.ts b/packages/mui-base/test/index.ts
new file mode 100644
index 0000000000..19595a45e5
--- /dev/null
+++ b/packages/mui-base/test/index.ts
@@ -0,0 +1,2 @@
+export { createRenderer } from './createRenderer';
+export { describeConformance } from './describeConformance';