HTML Attributes & contenteditable

Understanding which HTML attributes work with contenteditable elements, browser differences, and workarounds for unsupported attributes.

Overview

Unlike standard form inputs (<input> and <textarea>), contenteditable elements have limited support for HTML attributes. Many attributes that work on form inputs are either ignored, partially supported, or behave inconsistently across browsers.

Key Points

  • No Native Validation: Attributes like required, pattern, and maxlength are not supported
  • Limited Mobile Support: Mobile-specific attributes like inputmode and enterkeyhint often don't work
  • Browser Inconsistencies: Even supported attributes may behave differently across browsers
  • Manual Implementation Required: Many features must be implemented manually using JavaScript

Supported Attributes

These attributes generally work on contenteditable elements, though behavior may vary across browsers.

spellcheck

The spellcheck attribute controls browser spellchecking. It works on contenteditable, but may interfere with IME composition and editing flow.

<div contenteditable spellcheck="true">
  <!-- Spellcheck enabled -->
</div>

<div contenteditable spellcheck="false">
  <!-- Spellcheck disabled -->
</div>

⚠️ Spellcheck Interference

  • Spellcheck may interfere with IME composition - suggestions may appear during composition
  • Accepting spellcheck suggestions may cause caret to jump unexpectedly
  • Spellcheck UI may overlap with content during editing
  • Consider disabling spellcheck during composition (see code example below)
// Disable spellcheck during IME composition
const editor = document.querySelector('[contenteditable]');
let isComposing = false;

editor.addEventListener('compositionstart', () => {
  isComposing = true;
  editor.setAttribute('spellcheck', 'false');
});

editor.addEventListener('compositionend', () => {
  isComposing = false;
  editor.setAttribute('spellcheck', 'true');
});

lang

The lang attribute specifies the language of the content. It may affect spellcheck language in some browsers, but behavior is inconsistent.

<div contenteditable lang="en" spellcheck="true">
  <!-- English content -->
</div>

<div contenteditable lang="fr" spellcheck="true">
  <!-- French content -->
</div>

⚠️ lang May Not Affect Spellcheck

In Safari, the lang attribute does not affect spellcheck language. Spellcheck always uses the browser's default language, regardless of the lang value.

dir

The dir attribute controls text direction (ltr or rtl). It works on contenteditable, but changing it dynamically during editing may cause issues.

<div contenteditable dir="ltr">
  <!-- Left-to-right text -->
</div>

<div contenteditable dir="rtl">
  <!-- Right-to-left text -->
</div>

⚠️ Dynamic dir Changes

In Firefox, changing the dir attribute during active editing may not take effect immediately. The caret position may be incorrect, and text flow may not update properly. Save and restore selection when changing direction programmatically.

// Handle dir changes during editing
const editor = document.querySelector('[contenteditable]');

function setDirection(dir) {
  // Save selection
  const selection = window.getSelection();
  const range = selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : null;
  
  // Change direction
  editor.setAttribute('dir', dir);
  
  // Restore selection after a brief delay
  if (range) {
    requestAnimationFrame(() => {
      selection.removeAllRanges();
      selection.addRange(range);
    });
  }
}

Unsupported Attributes

These attributes are ignored by browsers on contenteditable elements. You must implement the functionality manually using JavaScript.

autofocus

The autofocus attribute does not work on contenteditable. Use JavaScript to focus the element on page load.

// Implement 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();
});

maxlength

The maxlength attribute is not supported. Implement length limiting manually using beforeinput events.

// Implement maxlength manually
const editor = document.querySelector('[contenteditable]');
const maxLength = 100;

editor.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'insertText' || e.inputType === 'insertCompositionText') {
    const currentLength = editor.textContent.length;
    const newText = e.data || '';
    const selection = window.getSelection();
    
    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      const selectedLength = range.toString().length;
      const newLength = currentLength - selectedLength + newText.length;
      
      if (newLength > maxLength) {
        e.preventDefault();
        // Optionally show warning to user
      }
    }
  }
});

required

The required attribute does not trigger form validation. Implement validation manually.

// Manual required validation
const editor = document.querySelector('[contenteditable]');
const form = editor.closest('form');

form.addEventListener('submit', (e) => {
  if (editor.textContent.trim() === '') {
    e.preventDefault();
    // Show validation error
    editor.setAttribute('aria-invalid', 'true');
  }
});

pattern

The pattern attribute does not validate content. Implement pattern validation manually.

// Manual pattern validation
const editor = document.querySelector('[contenteditable]');
const pattern = /^[0-9]+$/; // Numbers only

editor.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'insertText' || e.inputType === 'insertCompositionText') {
    const newText = e.data || '';
    const currentText = editor.textContent;
    const selection = window.getSelection();
    
    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      const selectedText = range.toString();
      const newContent = currentText.slice(0, range.startOffset) + 
                        newText + 
                        currentText.slice(range.endOffset);
      
      if (!pattern.test(newContent)) {
        e.preventDefault();
      }
    }
  }
});

readonly

The readonly attribute is ignored in Firefox. In other browsers, behavior is inconsistent. Implement readonly manually.

// Implement readonly manually
const editor = document.querySelector('[contenteditable]');
let isReadonly = false;

editor.addEventListener('beforeinput', (e) => {
  if (isReadonly) {
    e.preventDefault();
  }
});

// Also prevent paste, drag-drop, etc.
editor.addEventListener('paste', (e) => {
  if (isReadonly) {
    e.preventDefault();
  }
});

editor.addEventListener('drop', (e) => {
  if (isReadonly) {
    e.preventDefault();
  }
});

disabled

The disabled attribute is ignored in Safari. Implement disabled state manually.

// Manual disabled implementation
const editor = document.querySelector('[contenteditable]');
let isDisabled = false;

function setDisabled(disabled) {
  isDisabled = disabled;
  editor.setAttribute('contenteditable', disabled ? 'false' : 'true');
  editor.setAttribute('aria-disabled', disabled ? 'true' : 'false');
  editor.style.opacity = disabled ? '0.6' : '1';
  editor.style.pointerEvents = disabled ? 'none' : 'auto';
}

editor.addEventListener('beforeinput', (e) => {
  if (isDisabled) {
    e.preventDefault();
  }
});

autocomplete

The autocomplete attribute does not trigger browser autocomplete suggestions. Browser autocomplete features are not available for contenteditable.

ℹ️ No Browser Autocomplete

Browser autocomplete suggestions (for forms, addresses, etc.) do not appear when typing in contenteditable regions, even when appropriate autocomplete attributes are set. You must implement custom autocomplete if needed.

Partially Supported Attributes

These attributes work in some browsers or scenarios, but have limitations or inconsistencies.

placeholder

The placeholder attribute does not work on contenteditable. You must implement it using CSS and JavaScript.

// CSS-based placeholder implementation
[contenteditable]:empty::before {
  content: attr(data-placeholder);
  color: #999;
  pointer-events: none;
}

// JavaScript to handle focus behavior
const editor = document.querySelector('[contenteditable]');
editor.addEventListener('focus', () => {
  if (editor.textContent.trim() === '') {
    // Placeholder should remain visible
    // But Safari may hide it - need workaround
  }
});

editor.addEventListener('blur', () => {
  if (editor.textContent.trim() === '') {
    // Show placeholder again
  }
});

⚠️ Placeholder Disappears on Focus

In Safari, when using CSS ::before or ::after for placeholder, the placeholder disappears immediately when the element receives focus, even if the content is empty. This differs from standard input elements where placeholder persists until text is entered.

inputmode

The inputmode attribute should control the type of virtual keyboard on mobile devices, but it is ignored on contenteditable in iOS Safari.

<div contenteditable inputmode="numeric">
  <!-- Should show numeric keyboard, but doesn't work on iOS -->
</div>

<div contenteditable inputmode="email">
  <!-- Should show email keyboard, but doesn't work on iOS -->
</div>

⚠️ inputmode Not Supported on iOS

In iOS Safari, the inputmode attribute is ignored on contenteditable. The default keyboard always appears. Numeric, email, or URL keyboards cannot be triggered.

autocapitalize

The autocapitalize attribute works inconsistently on contenteditable in iOS Safari. Behavior may differ from standard input elements.

<div contenteditable autocapitalize="sentences">
  <!-- Should capitalize first letter of sentences -->
</div>

<div contenteditable autocapitalize="words">
  <!-- Should capitalize first letter of words -->
</div>

<div contenteditable autocapitalize="none">
  <!-- Should not capitalize -->
</div>

⚠️ autocapitalize Inconsistency

In Safari on iOS, autocapitalize may not work as expected on contenteditable. Capitalization behavior may differ from standard inputs, and the attribute may be ignored in some cases.

autocorrect

The autocorrect attribute behaves differently on contenteditable compared to standard input elements in iOS Safari.

<div contenteditable autocorrect="on">
  <!-- Should enable autocorrect -->
</div>

<div contenteditable autocorrect="off">
  <!-- Should disable autocorrect -->
</div>

⚠️ autocorrect May Not Respect Attribute

In Safari on iOS, autocorrect may not respect the attribute value on contenteditable. Autocorrect suggestions may appear even when autocorrect="off" is set, and behavior may differ from standard input elements.

enterkeyhint

The enterkeyhint attribute should control the label on the Enter key on mobile keyboards, but it is ignored on contenteditable in Android Chrome.

<div contenteditable enterkeyhint="send">
  <!-- Should show "Send" on Enter key, but doesn't work on Android -->
</div>

<div contenteditable enterkeyhint="search">
  <!-- Should show "Search" on Enter key, but doesn't work on Android -->
</div>

⚠️ enterkeyhint Not Supported on Android

In Chrome on Android, the enterkeyhint attribute is ignored on contenteditable. The Enter key always shows the default label, and no customization is possible.

Platform-Specific Issues & Edge Cases

Browser-Specific Behavior

Safari

  • placeholder: CSS-based placeholder disappears on focus even if content is empty
  • lang: Does not affect spellcheck language - always uses browser default
  • disabled: Attribute is ignored - element remains editable
  • inputmode: Ignored on iOS - default keyboard always appears
  • autocapitalize/autocorrect: Works inconsistently compared to standard inputs

Chrome/Edge

  • autocomplete: Attribute is ignored - no browser autocomplete suggestions
  • maxlength: Not supported - must implement manually
  • enterkeyhint: Ignored on Android - default Enter key label always shown

Firefox

  • readonly: Attribute is ignored - users can still edit
  • dir: Dynamic changes during editing may not take effect immediately - caret position may be incorrect

OS & Keyboard-Specific Behavior

iOS

  • inputmode: Completely ignored on contenteditable - cannot control keyboard type
  • autocapitalize: Works inconsistently - may not respect attribute value
  • autocorrect: May appear even when disabled - behavior differs from standard inputs

Android

  • enterkeyhint: Ignored on contenteditable - default Enter key label always shown
  • inputmode: May work in some browsers but behavior is inconsistent

Device-Specific Behavior

Mobile

  • Virtual Keyboard Attributes: Most mobile-specific attributes (inputmode, enterkeyhint, autocapitalize, autocorrect) work inconsistently or not at all
  • Touch Interaction: Some attributes may behave differently due to touch vs. mouse interaction

Tablet

  • Hybrid Behavior: Tablets may show desktop or mobile behavior depending on browser and OS
  • Keyboard Type: Physical keyboard vs. virtual keyboard affects attribute behavior

Best Practices

  • Always Implement Validation Manually: Don't rely on HTML attributes for validation - use JavaScript with beforeinput events
  • Test Across Browsers: Attribute support varies significantly - test on Safari, Chrome, Firefox, and mobile browsers
  • Provide Fallbacks: For unsupported attributes, provide JavaScript-based alternatives
  • Use ARIA Attributes: For accessibility, use ARIA attributes (aria-required, aria-invalid, etc.) alongside manual validation
  • Handle Mobile Separately: Mobile-specific attributes often don't work - implement custom solutions for mobile keyboards
  • Disable Spellcheck During IME: Spellcheck can interfere with IME composition - disable it during composition
  • Save/Restore Selection: When changing attributes dynamically (like dir), save and restore selection to maintain caret position