Skip to main content

Overview

Dialogs are high-level components used to render modal popups such as preferences, repository settings, and error messages. They’re built on the HTML5 <dialog> element and are shown as modals, constraining tab navigation within the dialog itself.

Basic Structure

A typical dialog follows this structure:
<Dialog title='Title'>
  <TabBar>...</TabBar>
  <DialogContent>
    ...
  </DialogContent>
  <DialogFooter>
    <OkCancelButtonGroup />
  </DialogFooter>
</Dialog>

Dialog Component

The main Dialog component accepts several important props:
interface IDialogProps {
  // Dialog title - renders a DialogHeader with icon and close button
  readonly title?: string | JSX.Element
  
  // Control dismissal behavior
  readonly backdropDismissable?: boolean  // Default: true
  readonly dismissDisabled?: boolean       // Default: false
  
  // Event handlers
  readonly onDismissed?: () => void
  readonly onSubmit?: () => void
  
  // Visual styling
  readonly type?: 'normal' | 'warning' | 'error'
  readonly loading?: boolean
  
  // Form state
  readonly disabled?: boolean
}

Example Implementation

From app/src/ui/dialog/dialog.tsx:252:
export class Dialog extends React.Component<DialogProps, IDialogState> {
  public static contextType = DialogStackContext
  public declare context: React.ContextType<typeof DialogStackContext>

  public render() {
    const className = classNames(
      {
        error: this.props.type === 'error',
        warning: this.props.type === 'warning',
      },
      this.props.className,
      'tooltip-host'
    )

    return (
      <dialog
        ref={this.onDialogRef}
        id={this.props.id}
        role={this.props.role}
        onMouseDown={this.onDialogMouseDown}
        onKeyDown={this.onKeyDown}
        className={className}
        {...this.getAriaAttributes()}
        tabIndex={-1}
      >
        {this.renderHeader()}
        <form onSubmit={this.onSubmit} onReset={this.onDismiss}>
          <fieldset disabled={this.props.disabled}>
            {this.props.children}
          </fieldset>
        </form>
      </dialog>
    )
  }
}

OkCancelButtonGroup

The OkCancelButtonGroup component handles platform-specific button ordering automatically:
  • Windows/Linux: Ok, Cancel
  • macOS: Cancel, Ok
This follows platform conventions as outlined in Nielsen Norman Group’s research on button order.

Basic Usage

<DialogFooter>
  <OkCancelButtonGroup />
</DialogFooter>

Customization Options

From app/src/ui/dialog/ok-cancel-button-group.tsx:5:
interface IOkCancelButtonGroupProps {
  // Control destructive actions
  readonly destructive?: boolean
  
  // Customize button text
  readonly okButtonText?: string | JSX.Element
  readonly cancelButtonText?: string | JSX.Element
  
  // Button state
  readonly okButtonDisabled?: boolean
  readonly cancelButtonDisabled?: boolean
  
  // Custom event handlers
  readonly onOkButtonClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
  readonly onCancelButtonClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
}

Destructive Dialogs

For destructive actions (hard to recover from), set destructive={true} to make the Cancel button the default.
<OkCancelButtonGroup 
  destructive={true}
  okButtonText="Delete"
  cancelButtonText="Keep"
/>
The destructive prop:
  • Makes the Cancel button the submit button (default action)
  • Prevents accidental destructive actions
  • Does not change which button triggers onSubmit vs onDismissed

Error Handling

Inline Errors

Dialogs should render errors inline using the DialogError component rather than opening new error dialogs. From app/src/ui/dialog/error.tsx:16:
export class DialogError extends React.Component {
  public render() {
    return (
      <div className="dialog-banner dialog-error" role="alert">
        <Octicon symbol={octicons.stop} />
        <div>{this.props.children}</div>
      </div>
    )
  }
}

Usage Example

The DialogError component must be the first child of the Dialog element.
<Dialog title='Preferences'>
  <DialogError>
    Could not save ignore file. Permission denied.
  </DialogError>
  <TabBar>...</TabBar>
  <DialogContent>
    ...
  </DialogContent>
  <DialogFooter>
    <OkCancelButtonGroup />
  </DialogFooter>
</Dialog>

Error Content Guidelines

1

Use text-based content

Keep error content primarily text-based and concise.
2

Omit 'Error' prefix

Don’t include the word “Error” - the styling makes it evident.
3

Be specific

Provide actionable information about what went wrong.

Best Practices

Content Structure

DO: Let child components render DialogContent
// Good - child renders DialogContent
<Dialog title='Title'>
  <TabBar>...</TabBar>
  {this.renderActiveTab()}
  <DialogFooter>
    <OkCancelButtonGroup />
  </DialogFooter>
</Dialog>

// ChildComponent.tsx
<DialogContent>
  my fancy content
</DialogContent>
DON’T: Wrap children inside DialogContent
// Bad - unnecessary nesting
<Dialog title='Title'>
  <TabBar>...</TabBar>
  <DialogContent>
    {this.renderActiveTab()}
  </DialogContent>
  <DialogFooter>
    <OkCancelButtonGroup />
  </DialogFooter>
</Dialog>

Layout Components

DO: Use Row components for layout The Row component receives bottom margin when used as an immediate child of DialogContent, making it excellent for structuring content. For primary text content, use <p> elements instead of Row.

Accessibility

Focus Management

Dialogs automatically manage focus based on this priority order:
1

Preferred focus element

Element with DialogPreferredFocusClassName class
2

Lowest positive tabIndex

Element with the lowest explicit tab index
3

First tabbable element

First input, textarea, or tabIndex=0 element
4

First submit button

Default action button
5

Any button

Remaining focusable buttons
6

Close button

Dialog dismiss button

ARIA Attributes

For alert dialogs that interrupt user workflow:
interface IAlertDialogProps {
  readonly role: 'alertdialog'
  readonly ariaDescribedBy: string  // Required for alertdialog role
}

Dialog Lifecycle

Dismissal Grace Period

Dialogs implement a 250ms grace period after mounting before acknowledging dismissal:
const dismissGracePeriodMs = 250

interface IDialogState {
  // Prevents accidental dismissal during grace period
  readonly isAppearing: boolean
}
This prevents users from accidentally dismissing important dialogs that appear while they’re clicking elsewhere.

Form Submission

All dialogs contain a top-level form element:
  • Submit: Triggers onSubmit event (affirmative action)
  • Reset: Triggers onDismissed event (cancel action)
  • Keyboard shortcuts: Ctrl/Cmd+W or Escape dismisses the dialog

Common Patterns

Simple Confirmation Dialog

<Dialog 
  title="Confirm Action"
  role="alertdialog"
  ariaDescribedBy="confirmation-message"
  onSubmit={this.handleConfirm}
  onDismissed={this.handleCancel}
>
  <DialogContent>
    <p id="confirmation-message">
      Are you sure you want to proceed?
    </p>
  </DialogContent>
  <DialogFooter>
    <OkCancelButtonGroup 
      okButtonText="Proceed"
      cancelButtonText="Cancel"
    />
  </DialogFooter>
</Dialog>

Loading State

<Dialog 
  title="Saving Changes"
  loading={this.state.isSaving}
  disabled={this.state.isSaving}
>
  <DialogContent>
    {/* Content */}
  </DialogContent>
  <DialogFooter>
    <OkCancelButtonGroup okButtonDisabled={this.state.isSaving} />
  </DialogFooter>
</Dialog>

Non-dismissable Dialog

<Dialog 
  title="Processing"
  dismissDisabled={true}
  backdropDismissable={false}
>
  {/* Content */}
</Dialog>

Build docs developers (and LLMs) love