Focus & Blur Events

Understanding focus and blur events in contenteditable elements, including autofocus limitations, focus management, nested element interactions, and browser differences.

Overview

Focus and blur events are crucial for managing the editing state of contenteditable elements. However, contenteditable has several limitations compared to standard form inputs, including the lack of autofocus support and issues with focus loss when interacting with nested elements.

Key Concepts

  • Focus Events: focus (doesn't bubble), focusin (bubbles)
  • Blur Events: blur (doesn't bubble), focusout (bubbles)
  • activeElement: document.activeElement to check which element has focus
  • autofocus: Not supported on contenteditable - must use JavaScript
  • tabindex: Use tabindex="0" for natural tab order

Focus & Blur Events

Contenteditable elements support standard focus and blur events. focus and blur don't bubble, while focusin and focusout do bubble.

// Focus and blur event handling
const editor = document.querySelector('[contenteditable]');

// Focus events
editor.addEventListener('focus', (e) => {
  console.log('Editor received focus');
  // Update UI, show toolbar, etc.
});

editor.addEventListener('focusin', (e) => {
  // Fires before focus, bubbles
  console.log('Focus entering editor');
});

// Blur events
editor.addEventListener('blur', (e) => {
  console.log('Editor lost focus');
  // Hide toolbar, save content, etc.
});

editor.addEventListener('focusout', (e) => {
  // Fires before blur, bubbles
  console.log('Focus leaving editor');
});

Event Differences

  • focus vs focusin: focus doesn't bubble, focusin bubbles - use focusin for event delegation
  • blur vs focusout: blur doesn't bubble, focusout bubbles - use focusout for event delegation
  • Timing: focusin fires before focus, focusout fires before blur

autofocus Limitation

The autofocus attribute does not work on contenteditable elements. You must use JavaScript to focus the element on page load.

// Implementing autofocus for contenteditable
const editor = document.querySelector('[contenteditable]');

// Option 1: On page load
window.addEventListener('load', () => {
  editor.focus();
});

// Option 2: Using requestAnimationFrame (better timing)
requestAnimationFrame(() => {
  editor.focus();
});

// Option 3: Using DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
  editor.focus();
});

// Option 4: After a short delay (if needed)
setTimeout(() => {
  editor.focus();
}, 100);

⚠️ autofocus Not Supported

The autofocus attribute, which automatically focuses form inputs on page load, does not work on contenteditable elements. There is no built-in way to automatically focus a contenteditable region when a page loads. You must use JavaScript with focus() method.

Focus Loss Issues

When a contenteditable region contains interactive elements (buttons, links, etc.), clicking on these elements may cause the contenteditable to lose focus. This interrupts the editing flow.

// Handling focus loss when clicking nested elements
const editor = document.querySelector('[contenteditable]');

// Prevent focus loss on nested interactive elements
editor.addEventListener('mousedown', (e) => {
  const target = e.target;
  
  // If clicking on button, link, etc. within editor
  if (target.tagName === 'BUTTON' || target.tagName === 'A') {
    // Prevent default to keep focus on editor
    e.preventDefault();
    
    // Handle the click manually
    handleButtonClick(target);
    
    // Restore focus to editor
    requestAnimationFrame(() => {
      editor.focus();
    });
  }
});

// Alternative: Use focusin/focusout to track focus
let isEditorFocused = false;

editor.addEventListener('focusin', () => {
  isEditorFocused = true;
});

editor.addEventListener('focusout', (e) => {
  // Check if focus is moving to a child element
  const relatedTarget = e.relatedTarget;
  
  if (relatedTarget && editor.contains(relatedTarget)) {
    // Focus is moving to a child - keep editor focused
    requestAnimationFrame(() => {
      editor.focus();
    });
  } else {
    // Focus is leaving editor completely
    isEditorFocused = false;
  }
});

⚠️ Focus Lost on Nested Elements

In Firefox on Windows, clicking interactive elements (buttons, links) within contenteditable removes focus from the contenteditable. The caret disappears, and typing no longer inserts text. You must manually restore focus or prevent the default behavior.

Preventing Focus Loss

  • Use mousedown event to prevent default on nested interactive elements
  • Use focusout to detect when focus is leaving and restore it if moving to a child
  • Handle clicks on nested elements manually and restore focus after
  • Consider using contenteditable="false" for interactive elements, but be aware of its limitations

Checking Focus State

Use document.activeElement to check which element currently has focus.

// Checking focus state
const editor = document.querySelector('[contenteditable]');

// Check if editor is focused
function isEditorFocused() {
  return document.activeElement === editor;
}

// Monitor focus changes
document.addEventListener('focusin', (e) => {
  if (e.target === editor) {
    console.log('Editor gained focus');
  }
});

document.addEventListener('focusout', (e) => {
  if (e.target === editor) {
    console.log('Editor lost focus');
  }
});

// Check focus state periodically (if needed)
setInterval(() => {
  if (isEditorFocused()) {
    // Editor is focused
  }
}, 100);

activeElement Notes

  • Read-only: document.activeElement is read-only - use focus() to change focus
  • null: May be null when no element has focus (e.g., clicking outside)
  • body: May be document.body in some cases
  • Shadow DOM: In Shadow DOM, use shadowRoot.activeElement

tabindex Management

Use tabindex to control tab order. For natural tab order, use tabindex="0". Custom tabindex values may not work correctly in some browsers.

// Managing tabindex for focus order
const editors = document.querySelectorAll('[contenteditable]');

// Set tabindex for natural tab order
editors.forEach((editor, index) => {
  editor.setAttribute('tabindex', '0');
});

// Or manage focus programmatically
editors.forEach((editor, index) => {
  editor.addEventListener('keydown', (e) => {
    if (e.key === 'Tab' && !e.shiftKey) {
      e.preventDefault();
      const next = editors[index + 1];
      if (next) {
        next.focus();
      }
    } else if (e.key === 'Tab' && e.shiftKey) {
      e.preventDefault();
      const prev = editors[index - 1];
      if (prev) {
        prev.focus();
      }
    }
  });
});

⚠️ tabindex Issues

When multiple contenteditable regions have tabindex attributes, the tab order may not follow the tabindex values correctly in some browsers. The focus order may be inconsistent or incorrect. Use tabindex="0" for natural DOM order, or manage focus programmatically.

Programmatic Focus Management

You can programmatically focus and blur contenteditable elements, and control cursor position.

// Programmatic focus management
const editor = document.querySelector('[contenteditable]');

// Focus the editor
function focusEditor() {
  editor.focus();
  
  // Optionally move cursor to end
  const selection = window.getSelection();
  const range = document.createRange();
  range.selectNodeContents(editor);
  range.collapse(false); // Collapse to end
  selection.removeAllRanges();
  selection.addRange(range);
}

// Blur the editor
function blurEditor() {
  editor.blur();
}

// Focus with cursor at specific position
function focusAtPosition(offset) {
  editor.focus();
  const selection = window.getSelection();
  const range = document.createRange();
  
  if (editor.firstChild) {
    range.setStart(editor.firstChild, offset);
    range.collapse(true);
    selection.removeAllRanges();
    selection.addRange(range);
  }
}

Focus Best Practices

  • Timing: Use requestAnimationFrame for better timing when focusing after DOM changes
  • Cursor Position: After focusing, set cursor position using Selection API
  • User Interaction: Only focus programmatically in response to user actions (accessibility requirement)
  • Blur Carefully: Be careful when blurring - it may interrupt user editing

Page Visibility & Focus

When the page becomes hidden (e.g., user switches tabs), you may want to blur the editor. When the page becomes visible again, you can optionally restore focus.

// Handling focus when page visibility changes
const editor = document.querySelector('[contenteditable]');

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // Page is hidden - may want to blur editor
    if (document.activeElement === editor) {
      editor.blur();
    }
  } else {
    // Page is visible again
    // Optionally restore focus
    // editor.focus();
  }
});

Visibility Considerations

  • Blur on Hide: Consider blurring editor when page is hidden to prevent unexpected behavior
  • Restore on Show: Generally don't auto-restore focus when page becomes visible (accessibility)
  • Save State: Save editor state before blurring if needed

Platform-Specific Issues & Edge Cases

Browser-Specific Behavior

Chrome/Edge

  • autofocus: Attribute is ignored - must use JavaScript
  • Focus Timing: May need requestAnimationFrame for reliable focus

Firefox

  • Focus Loss: Clicking nested interactive elements removes focus from contenteditable
  • tabindex: Custom tabindex values may not work correctly

Safari

  • Focus Behavior: Generally consistent, but may differ on mobile
  • Mobile Focus: Virtual keyboard may affect focus behavior

OS & Device-Specific Behavior

Desktop

  • Mouse Interaction: Clicking focuses the editor
  • Keyboard Navigation: Tab key moves focus between elements

Mobile

  • Touch Interaction: Tapping focuses the editor and shows virtual keyboard
  • Virtual Keyboard: Keyboard appearance/disappearance may trigger focus/blur events
  • Focus Timing: Focus events may fire at different times due to keyboard animation

General Edge Cases

  • Nested contenteditable: Focus behavior may be complex with nested editable regions
  • contenteditable="false": Focus may not work correctly with non-editable areas
  • Shadow DOM: Focus behavior differs when contenteditable is inside Shadow DOM
  • iframe: Focus behavior may differ when contenteditable is inside iframe
  • Programmatic Updates: DOM updates may cause focus loss - save and restore focus if needed

Best Practices

  • Use focusin/focusout: For event delegation, use focusin and focusout which bubble
  • Implement autofocus Manually: Use JavaScript with appropriate timing (requestAnimationFrame)
  • Prevent Focus Loss: Handle nested interactive elements to prevent unwanted focus loss
  • Use tabindex="0": For natural tab order, avoid custom tabindex values
  • Check activeElement: Use document.activeElement to check focus state
  • Save/Restore Focus: Save and restore focus when making DOM updates
  • Handle Visibility: Consider blurring editor when page is hidden
  • Test Across Browsers: Focus behavior varies - test on Chrome, Firefox, Safari, and mobile browsers