Skip to content

Focus Project — Build an Accessible Dialog With Full Focus Management

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about Focus Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a production-ready accessible dialog with focus trapping, focus restoration, inert background, aria-modal, Escape handling, and full keyboard support.

In this project, you'll implement complete Focus Management.

What You'll Learn

By the end of this project, you'll have built a fully accessible dialog component that handles all focus management requirements.

Why It Matters

Modal dialogs are the most common focus management challenge. Mastering this pattern prepares you for all overlay components.

Real-World Use

Doda Browser's settings dialog uses this exact pattern, as does Durga Antivirus Pro's threat confirmation dialog.

Project Architecture

flowchart TD
  A[Dialog] --> B[Open: store trigger]
  A --> C[Open: show dialog]
  A --> D[Open: inert background]
  A --> E[Open: trap focus]
  A --> F[Open: focus first element]
  A --> G[Close: restore focus]
  A --> H[Close: remove inert]
  A --> I[Close: hide dialog]

HTML Structure

<button id="settings-trigger" aria-haspopup="dialog">
  Open Settings
</button>

<div
  id="settings-dialog"
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  hidden
  class="dialog"
>
  <div class="dialog-overlay"></div>
  <div class="dialog-content">
    <h2 id="dialog-title">Settings</h2>
    <label>
      Username
      <input type="text" id="username" />
    </label>
    <label>
      Email
      <input type="email" id="email" />
    </label>
    <div class="dialog-actions">
      <button id="save-btn" class="primary">Save</button>
      <button id="close-btn">Cancel</button>
    </div>
  </div>
</div>

Complete Implementation

class AccessibleDialog {
  constructor(dialogElement) {
    this.dialog = dialogElement;
    this.trigger = document.querySelector('[aria-haspopup="dialog"]');
    this.lastFocused = null;

    this.dialog.addEventListener('keydown', (e) => this.handleKeydown(e));
    this.dialog.querySelector('.dialog-overlay').addEventListener('click', () => this.close());
    this.dialog.querySelector('#close-btn').addEventListener('click', () => this.close());
    this.dialog.querySelector('#save-btn').addEventListener('click', () => this.save());
    this.trigger.addEventListener('click', () => this.open());

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && !this.dialog.hidden) this.close();
    });
  }

  open() {
    this.lastFocused = document.activeElement;
    this.dialog.hidden = false;
    document.getElementById('page-content').inert = true;

    const firstFocusable = this.dialog.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    if (firstFocusable) {
      firstFocusable.focus();
    }
  }

  close() {
    this.dialog.hidden = true;
    document.getElementById('page-content').inert = false;

    if (this.lastFocused && document.contains(this.lastFocused)) {
      this.lastFocused.focus();
    }
  }

  save() {
    const username = this.dialog.querySelector('#username').value;
    const email = this.dialog.querySelector('#email').value;
    console.log('Saved:', { username, email });
    this.close();
  }

  handleKeydown(event) {
    if (event.key !== 'Tab') return;

    const focusable = this.dialog.querySelectorAll(
      'button:not([disabled]), [href], input:not([disabled]), ' +
      'select:not([disabled]), textarea:not([disabled]), ' +
      '[tabindex]:not([tabindex="-1"]):not([disabled])'
    );

    if (focusable.length === 0) return;

    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  }
}

new AccessibleDialog(document.getElementById('settings-dialog'));

CSS Styling

.dialog {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.dialog-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
}

.dialog-content {
  position: relative;
  background: #fff;
  padding: 24px;
  border-radius: 8px;
  max-width: 480px;
  width: 90%;
}

.dialog[hidden] {
  display: none;
}

Testing Checklist

  • Trigger focuses on Tab
  • Enter/Space opens the dialog
  • Focus moves to first input on open
  • Tab cycles through focusable elements
  • Shift+Tab cycles backward
  • Background is non-interactive (inert)
  • Escape closes the dialog
  • Overlay click closes the dialog
  • Focus returns to trigger on close
  • Screen reader announces dialog title

Common Mistakes

  • Not using inert on background: Users can Tab into background elements.
  • Forgetting Shift+Tab cycling: Keyboard users get stuck at the first element.
  • Not restoring focus on close: Users are stranded at the page top.
  • Not handling overlay click: Users cannot close by clicking outside.
  • Not setting aria-modal: Screen readers do not know the dialog is modal.

FAQ

Should I use the native element?

The native

has limited browser support for accessibility. A custom implementation with ARIA is more reliable.

What if the dialog has scrollable content?

Focus trapping should still work with scrollable content. The sentinel approach may be easier for long dialogs.

Should I close the dialog on Escape?

Yes. Escape to close is a standard accessibility pattern.

What if the trigger is removed during the dialog?

Store the trigger reference on open and provide a fallback if it disappears.

Can I animate the dialog opening?

Yes, but ensure the animation completes before moving focus.

Mini Project

You just built the mini project. Add confirmation dialog support: when the user clicks Save, show a confirmation dialog. Manage focus across both dialogs.

What's Next

This completes the Focus Management Deep Dive series. Continue to Accessible Modals and Dialogs for more advanced dialog patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro