Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

- Dialog boxes consist of a header (`DialogTitle`), content (`DialogContent`), and footer (`DialogActions`), which should all be included inside a body (`DialogBody`).
- Validate that people’s entries are acceptable before closing the dialog. Show an inline validation error near the field they must correct.
- Modal dialogs should be used very sparingly—only when it’s critical that people make a choice or provide information before they can proceed. Thee dialogs are generally used for irreversible or potentially destructive tasks. They’re typically paired with an backdrop without a light dismiss.
- Modal dialogs should be used very sparingly—only when it’s critical that people make a choice or provide information before they can proceed. These dialogs are generally used for irreversible or potentially destructive tasks. They’re typically paired with a backdrop without a light dismiss.
- Add a `aria-describedby` attribute on `DialogSurface` pointing to the dialog content on short confirmation like dialogs.
- Add a `aria-label` or `aria-labelledby` attribute on `DialogSurface` if there is no `DialogTitle`

### Don't

- Don't use more than three buttons between `DialogActions`.
- Don't open a `Dialog` from a `Dialog`
- Don't open nested `Dialog`s. They are an anti-pattern and should be avoided. Nested dialogs create complex focus restoration logic and confuse users. If your design requires stacking dialogs, consider using a multi-step wizard within a single dialog, sequential dialogs, or a different UI component (panels, sidebars, popovers).
- Don't use a `Dialog` with no focusable elements
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Nested Dialogs
Comment thread
paolo-aliprandi marked this conversation as resolved.

Nested dialogs (opening a dialog from within another dialog) are **an anti-pattern and should be avoided** whenever possible. Even when implementing proper focus management, nested dialogs create complex focus restoration logic and confuse users about their context.

## Why Nested Dialogs Are Problematic

1. **Focus Management Complexity** - Multiple dialog layers require careful keyboard and focus handling, increasing the risk of navigation bugs
2. **User Confusion** - Users lose sense of their context when multiple overlays stack on top of each other
3. **Accessibility Challenges** - Screen reader users and keyboard-only users struggle to manage multiple modal overlays
4. **Anti-pattern by Design** - Modal dialogs are intentionally disruptive; stacking them compounds this problem

## What to Do Instead

**Redesign your workflow** to eliminate the need for nested dialogs:

- **Single multi-step flow** - Use tabs, accordions, or numbered steps within a single dialog
- **Sequential dialogs** - Close the first dialog before opening the next one
- **Different UI patterns** - Consider panels, sidebars, popovers, or non-modal overlays
- **Inline content** - Expand/collapse sections within the dialog instead of opening new dialogs

## If You Must Use Nested Dialogs (Rare)

Should your design truly require nested dialogs:

1. Use `DialogTrigger` for user-triggered opens (it automatically restores focus)
2. Use `useRestoreFocusTarget()` on elements that programmatically open dialogs
3. Test thoroughly with keyboard navigation (Escape, Tab, Shift+Tab)
4. Verify focus restoration works correctly for screen readers

For details on focus management utilities, see the [focus management documentation](https://react.fluentui.dev/?path=/docs/utilities-focus-management--docs).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used to have a story for Nested dialogs usage before also. It was removed since, even though we clearly stated that is a bad pattern, people kept using it all the time. Our conclusion was: if this is a bad pattern we should most likely not be explaining how to "do it properly", it's an anti-pattern and we discourage it, find another solution.

I would keep it as a section and an explanation, but I would avoid providing an example entirely, just to discourage even more the usage of it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I removed the anti-pattern example

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';
import type { JSXElement } from '@fluentui/react-components';
import {
Dialog,
DialogTrigger,
DialogSurface,
DialogTitle,
DialogBody,
DialogContent,
DialogActions,
Button,
} from '@fluentui/react-components';
import story from './DialogNestedDialogs.md';

export const NestedDialogsWithTrigger = (): JSXElement => {
return (
<Dialog>
<DialogTrigger disableButtonEnhancement>
<Button appearance="primary">Open Outer Dialog</Button>
</DialogTrigger>

<DialogSurface>
<DialogBody>
<DialogTitle>Outer Dialog</DialogTitle>
<DialogContent>
This is the outer dialog. Click the button below to open a nested dialog. When using DialogTrigger, focus is
automatically restored.
</DialogContent>
<DialogActions>
<Dialog>
<DialogTrigger disableButtonEnhancement>
<Button appearance="primary">Open Inner Dialog</Button>
</DialogTrigger>

<DialogSurface>
<DialogBody>
<DialogTitle>Inner Dialog</DialogTitle>
<DialogContent>
This is a nested dialog inside the outer dialog. Focus will automatically be restored to the Open
Inner Dialog button when this one closes thanks to DialogTrigger.
</DialogContent>
<DialogActions>
<Button appearance="primary">Confirm</Button>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary">Close Inner Dialog</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>

<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary">Close Outer Dialog</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};

NestedDialogsWithTrigger.parameters = {
docs: {
description: {
story,
},
},
Comment thread
paolo-aliprandi marked this conversation as resolved.
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
When using a `Dialog` without a `DialogTrigger` (or when using a `DialogTrigger` outside of a `Dialog`), it becomes your responsibility to control some of the dialog's behavior.
When using a `Dialog` without a `DialogTrigger`, you become responsible for managing the dialog's behavior. This applies to:

1. You must make sure that the `open` state is set accordingly to the dialog's visibility (mostly this means to properly react to the events provided by `onOpenChange` callback on `Dialog` component).
2. You must make sure that focus is properly restored once the dialog is closed (this can be achieved by using the `useRestoreFocusTarget` hook, or by manually invoking `.focus()` on the target element).
- Opening dialogs programmatically (via state, API calls, side effects)
- Opening nested dialogs where the inner dialog is not wrapped in a `DialogTrigger`

The example bellow showcases both explicit responsibilities:
**Your responsibilities:**

1. **Control the open state** - React to the `onOpenChange` callback and ensure the `open` state reflects the dialog's visibility
2. **Restore focus** - When the dialog closes, you must restore focus to the element that triggered the open. Use `useRestoreFocusTarget` on the trigger element, or manually invoke `.focus()` on the target element. `DialogSurface` already applies the restore-focus source attributes internally when used inside `Dialog`.

The example below showcases both responsibilities:
Loading