diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties
index db404c896..56ccca1cd 100644
--- a/l10n/en-US/viewer.properties
+++ b/l10n/en-US/viewer.properties
@@ -263,3 +263,8 @@ editor_stamp_add_image.title=Add image
 editor_free_text2_aria_label=Text Editor
 editor_ink2_aria_label=Draw Editor
 editor_ink_canvas_aria_label=User-created image
+
+# Alt-text dialog
+# LOCALIZATION NOTE (alt_text_button_label): Alternative text (alt text) helps
+# when people can't see the image.
+alt_text_button_label=Alt text
diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js
index 0641248fe..093402c66 100644
--- a/src/display/editor/editor.js
+++ b/src/display/editor/editor.js
@@ -34,6 +34,8 @@ import { FeatureTest, shadow, unreachable } from "../../shared/util.js";
  * Base class for editors.
  */
 class AnnotationEditor {
+  #altTextButton = null;
+
   #keepAspectRatio = false;
 
   #resizersDiv = null;
@@ -54,6 +56,8 @@ class AnnotationEditor {
 
   _focusEventsAllowed = true;
 
+  _l10nPromise = null;
+
   #isDraggable = false;
 
   #zIndex = AnnotationEditor._zIndex++;
@@ -64,6 +68,10 @@ class AnnotationEditor {
 
   static _zIndex = 1;
 
+  // When one of the dimensions of an editor is smaller than this value, the
+  // button to edit the alt text is visually moved outside of the editor.
+  static SMALL_EDITOR_SIZE = 0;
+
   /**
    * @param {AnnotationEditorParameters} parameters
    */
@@ -124,9 +132,17 @@ class AnnotationEditor {
 
   /**
    * Initialize the l10n stuff for this type of editor.
-   * @param {Object} _l10n
+   * @param {Object} l10n
    */
-  static initialize(_l10n) {
+  static initialize(l10n, options = null) {
+    AnnotationEditor._l10nPromise ||= new Map(
+      ["alt_text_button_label"].map(str => [str, l10n.get(str)])
+    );
+    if (options?.strings) {
+      for (const str of options.strings) {
+        AnnotationEditor._l10nPromise.set(str, l10n.get(str));
+      }
+    }
     if (AnnotationEditor._borderLineWidth !== -1) {
       return;
     }
@@ -522,6 +538,11 @@ class AnnotationEditor {
     if (!this.#keepAspectRatio) {
       this.div.style.height = `${((100 * height) / parentHeight).toFixed(2)}%`;
     }
+    this.#altTextButton?.classList.toggle(
+      "small",
+      width < AnnotationEditor.SMALL_EDITOR_SIZE ||
+        height < AnnotationEditor.SMALL_EDITOR_SIZE
+    );
   }
 
   fixDims() {
@@ -785,6 +806,40 @@ class AnnotationEditor {
     this.fixAndSetPosition();
   }
 
+  addAltTextButton() {
+    if (this.#altTextButton) {
+      return;
+    }
+    const altText = (this.#altTextButton = document.createElement("span"));
+    altText.className = "altText";
+    AnnotationEditor._l10nPromise.get("alt_text_button_label").then(msg => {
+      altText.textContent = msg;
+    });
+    altText.tabIndex = "0";
+    altText.addEventListener(
+      "click",
+      event => {
+        event.preventDefault();
+      },
+      { capture: true }
+    );
+    altText.addEventListener("keydown", event => {
+      if (event.target === altText && event.key === "Enter") {
+        event.preventDefault();
+      }
+    });
+    this.div.append(altText);
+    if (!AnnotationEditor.SMALL_EDITOR_SIZE) {
+      // We take the width of the alt text button and we add 40% to it to be
+      // sure to have enough space for it.
+      const PERCENT = 40;
+      AnnotationEditor.SMALL_EDITOR_SIZE = Math.min(
+        128,
+        Math.round(altText.getBoundingClientRect().width * (1 + PERCENT / 100))
+      );
+    }
+  }
+
   /**
    * Render this editor in a div.
    * @returns {HTMLDivElement}
@@ -1144,13 +1199,21 @@ class AnnotationEditor {
    * When the user disables the editing mode some editors can change some of
    * their properties.
    */
-  disableEditing() {}
+  disableEditing() {
+    if (this.#altTextButton) {
+      this.#altTextButton.hidden = true;
+    }
+  }
 
   /**
    * When the user enables the editing mode some editors can change some of
    * their properties.
    */
-  enableEditing() {}
+  enableEditing() {
+    if (this.#altTextButton) {
+      this.#altTextButton.hidden = false;
+    }
+  }
 
   /**
    * The editor is about to be edited.
diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js
index 385492c0c..efad76948 100644
--- a/src/display/editor/freetext.js
+++ b/src/display/editor/freetext.js
@@ -56,8 +56,6 @@ class FreeTextEditor extends AnnotationEditor {
 
   static _freeTextDefaultContent = "";
 
-  static _l10nPromise;
-
   static _internalPadding = 0;
 
   static _defaultColor = null;
@@ -145,13 +143,9 @@ class FreeTextEditor extends AnnotationEditor {
 
   /** @inheritdoc */
   static initialize(l10n) {
-    super.initialize(l10n);
-    this._l10nPromise = new Map(
-      ["free_text2_default_content", "editor_free_text2_aria_label"].map(
-        str => [str, l10n.get(str)]
-      )
-    );
-
+    AnnotationEditor.initialize(l10n, {
+      strings: ["free_text2_default_content", "editor_free_text2_aria_label"],
+    });
     const style = getComputedStyle(document.documentElement);
 
     if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
@@ -548,11 +542,11 @@ class FreeTextEditor extends AnnotationEditor {
     this.editorDiv.setAttribute("id", this.#editorDivId);
     this.enableEditing();
 
-    FreeTextEditor._l10nPromise
+    AnnotationEditor._l10nPromise
       .get("editor_free_text2_aria_label")
       .then(msg => this.editorDiv?.setAttribute("aria-label", msg));
 
-    FreeTextEditor._l10nPromise
+    AnnotationEditor._l10nPromise
       .get("free_text2_default_content")
       .then(msg => this.editorDiv?.setAttribute("default-content", msg));
     this.editorDiv.contentEditable = true;
diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js
index 7651b5451..f56929b05 100644
--- a/src/display/editor/ink.js
+++ b/src/display/editor/ink.js
@@ -62,8 +62,6 @@ class InkEditor extends AnnotationEditor {
 
   static _defaultThickness = 1;
 
-  static _l10nPromise;
-
   static _type = "ink";
 
   constructor(params) {
@@ -84,13 +82,9 @@ class InkEditor extends AnnotationEditor {
 
   /** @inheritdoc */
   static initialize(l10n) {
-    super.initialize(l10n);
-    this._l10nPromise = new Map(
-      ["editor_ink_canvas_aria_label", "editor_ink2_aria_label"].map(str => [
-        str,
-        l10n.get(str),
-      ])
-    );
+    AnnotationEditor.initialize(l10n, {
+      strings: ["editor_ink_canvas_aria_label", "editor_ink2_aria_label"],
+    });
   }
 
   /** @inheritdoc */
@@ -743,7 +737,7 @@ class InkEditor extends AnnotationEditor {
     this.canvas.width = this.canvas.height = 0;
     this.canvas.className = "inkEditorCanvas";
 
-    InkEditor._l10nPromise
+    AnnotationEditor._l10nPromise
       .get("editor_ink_canvas_aria_label")
       .then(msg => this.canvas?.setAttribute("aria-label", msg));
     this.div.append(this.canvas);
@@ -782,7 +776,7 @@ class InkEditor extends AnnotationEditor {
 
     super.render();
 
-    InkEditor._l10nPromise
+    AnnotationEditor._l10nPromise
       .get("editor_ink2_aria_label")
       .then(msg => this.div?.setAttribute("aria-label", msg));
 
diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js
index 61cb48fec..62651d51a 100644
--- a/src/display/editor/stamp.js
+++ b/src/display/editor/stamp.js
@@ -50,6 +50,11 @@ class StampEditor extends AnnotationEditor {
     this.#bitmapFile = params.bitmapFile;
   }
 
+  /** @inheritdoc */
+  static initialize(l10n) {
+    AnnotationEditor.initialize(l10n);
+  }
+
   static get supportedTypes() {
     // See https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
     // to know which types are supported by the browser.
@@ -306,6 +311,7 @@ class StampEditor extends AnnotationEditor {
       this.parent.addUndoableEditor(this);
       this.#hasBeenAddedInUndoStack = true;
     }
+    this.addAltTextButton();
   }
 
   /**
diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js
index f0177494d..47c26e88d 100644
--- a/src/display/editor/tools.js
+++ b/src/display/editor/tools.js
@@ -741,6 +741,14 @@ class AnnotationEditorUIManager {
     );
   }
 
+  get direction() {
+    return shadow(
+      this,
+      "direction",
+      getComputedStyle(this.#container).direction
+    );
+  }
+
   onPageChanging({ pageNumber }) {
     this.#currentPageIndex = pageNumber - 1;
   }
diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css
index abf1a2a11..2b7e3d621 100644
--- a/web/annotation_editor_layer_builder.css
+++ b/web/annotation_editor_layer_builder.css
@@ -37,6 +37,22 @@
   /*#else*/
   --editorInk-editing-cursor: url(images/cursor-editorInk.svg) 0 16, pointer;
   /*#endif*/
+
+  --alt-text-add-image: url(images/altText_add.svg);
+  --alt-text-done-image: url(images/altText_done.svg);
+  --alt-text-bg-color: #2b2a33;
+  --alt-text-fg-color: #fbfbfe;
+  --alt-text-border-color: var(--alt-text-bg-color);
+  --alt-text-hover-bg-color: #52525e;
+  --alt-text-hover-fg-color: var(--alt-text-fg-color);
+  --alt-text-hover-border-color: var(--alt-text-hover-bg-color);
+  --alt-text-active-bg-color: #5b5b66;
+  --alt-text-active-fg-color: var(--alt-text-fg-color);
+  --alt-text-active-border-color: var(--alt-text-hover-bg-color);
+  --alt-text-focus-outline-color: #0060df;
+  --alt-text-focus-border-color: #f0f0f4;
+  --alt-text-shadow: 0 2px 6px 0 rgba(28, 27, 34, 0.5);
+  --alt-text-opacity: 0.8;
 }
 
 @media (min-resolution: 1.1dppx) {
@@ -53,6 +69,20 @@
     --outline-color: CanvasText;
     --outline-around-color: ButtonFace;
     --resizer-bg-color: ButtonText;
+
+    --alt-text-bg-color: Canvas;
+    --alt-text-fg-color: ButtonText;
+    --alt-text-border-color: ButtonText;
+    --alt-text-hover-bg-color: Canvas;
+    --alt-text-hover-fg-color: SelectedItem;
+    --alt-text-hover-border-color: SelectedItem;
+    --alt-text-active-bg-color: ButtonFace;
+    --alt-text-active-fg-color: SelectedItem;
+    --alt-text-active-border-color: ButtonText;
+    --alt-text-focus-outline-color: CanvasText;
+    --alt-text-focus-border-color: ButtonText;
+    --alt-text-shadow: none;
+    --alt-text-opacity: 1;
   }
 }
 
@@ -331,4 +361,174 @@
       }
     }
   }
+
+  &
+    :is(
+      [data-main-rotation="0"] [data-editor-rotation="90"],
+      [data-main-rotation="90"] [data-editor-rotation="0"],
+      [data-main-rotation="180"] [data-editor-rotation="270"],
+      [data-main-rotation="270"] [data-editor-rotation="180"],
+
+    ) {
+    & .altText {
+      rotate: 270deg;
+
+      &:dir(ltr) {
+        inset-inline-start: calc(100% - 8px);
+
+        &.small {
+          inset-inline-start: calc(100% + 8px);
+          inset-block-start: 100%;
+        }
+      }
+
+      &:dir(rtl) {
+        inset-block-end: calc(100% - 8px);
+
+        &.small {
+          inset-inline-start: -8px;
+          inset-block-start: 0;
+        }
+      }
+    }
+  }
+
+  &
+    :is(
+      [data-main-rotation="0"] [data-editor-rotation="180"],
+      [data-main-rotation="90"] [data-editor-rotation="90"],
+      [data-main-rotation="180"] [data-editor-rotation="0"],
+      [data-main-rotation="270"] [data-editor-rotation="270"],
+
+    ) {
+    & .altText {
+      rotate: 180deg;
+
+      inset-block-end: calc(100% - 8px);
+      inset-inline-start: calc(100% - 8px);
+
+      &.small {
+        inset-inline-start: 100%;
+        inset-block-start: -8px;
+      }
+    }
+  }
+
+  &
+    :is(
+      [data-main-rotation="0"] [data-editor-rotation="270"],
+      [data-main-rotation="90"] [data-editor-rotation="180"],
+      [data-main-rotation="180"] [data-editor-rotation="90"],
+      [data-main-rotation="270"] [data-editor-rotation="0"],
+
+    ) {
+    & .altText {
+      rotate: 90deg;
+
+      &:dir(ltr) {
+        inset-block-end: calc(100% - 8px);
+
+        &.small {
+          inset-inline-start: -8px;
+          inset-block-start: 0;
+        }
+      }
+
+      &:dir(rtl) {
+        inset-inline-start: calc(100% - 8px);
+
+        &.small {
+          inset-inline-start: calc(100% + 8px);
+          inset-block-start: 100%;
+        }
+      }
+    }
+  }
+}
+
+.altText {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: auto;
+  height: 24px;
+  min-width: 88px;
+  z-index: 1;
+  pointer-events: all;
+
+  color: var(--alt-text-fg-color);
+  font: menu;
+  font-size: 12px;
+  border-radius: 4px;
+  border: 1px solid var(--alt-text-border-color);
+  opacity: var(--alt-text-opacity);
+  background-color: var(--alt-text-bg-color);
+  box-shadow: var(--alt-text-shadow);
+
+  position: absolute;
+  inset-block-end: 8px;
+  inset-inline-start: 8px;
+
+  &:dir(ltr) {
+    transform-origin: 0 100%;
+  }
+  &:dir(rtl) {
+    transform-origin: 100% 100%;
+  }
+
+  &.small {
+    &:dir(ltr) {
+      transform-origin: 0 0;
+    }
+    &:dir(rtl) {
+      transform-origin: 100% 0;
+    }
+
+    inset-block-end: unset;
+    inset-inline-start: 0;
+    inset-block-start: calc(100% + 8px);
+  }
+
+  &:hover {
+    background-color: var(--alt-text-hover-bg-color);
+    border-color: var(--alt-text-hover-border-color);
+    color: var(--alt-text-hover-fg-color);
+    cursor: pointer;
+
+    &::before {
+      background-color: var(--alt-text-hover-fg-color);
+    }
+  }
+
+  &:active {
+    background-color: var(--alt-text-active-bg-color);
+    border-color: var(--alt-text-active-border-color);
+    color: var(--alt-text-active-fg-color);
+
+    &::before {
+      background-color: var(--alt-text-active-fg-color);
+    }
+  }
+
+  &:focus-visible {
+    outline: 2px solid var(--alt-text-focus-outline-color);
+    border-color: var(--alt-text-focus-border-color);
+  }
+
+  &::before {
+    content: "";
+    mask-image: var(--alt-text-add-image);
+    mask-size: cover;
+    mask-repeat: no-repeat;
+    mask-position: center;
+    display: inline-block;
+    width: 12px;
+    height: 12px;
+    background-color: var(--alt-text-fg-color);
+    margin-inline-end: 4px;
+  }
+
+  &.done::before {
+    mask-image: var(--alt-text-done-image);
+  }
 }
diff --git a/web/annotation_editor_layer_builder.js b/web/annotation_editor_layer_builder.js
index a15ffa79a..a6f1bbe08 100644
--- a/web/annotation_editor_layer_builder.js
+++ b/web/annotation_editor_layer_builder.js
@@ -82,6 +82,7 @@ class AnnotationEditorLayerBuilder {
     div.className = "annotationEditorLayer";
     div.tabIndex = 0;
     div.hidden = true;
+    div.dir = this.#uiManager.direction;
     this.pageDiv.append(div);
 
     this.annotationEditorLayer = new AnnotationEditorLayer({
diff --git a/web/images/altText_add.svg b/web/images/altText_add.svg
new file mode 100644
index 000000000..3451b536c
--- /dev/null
+++ b/web/images/altText_add.svg
@@ -0,0 +1,3 @@
+ 
diff --git a/web/images/altText_done.svg b/web/images/altText_done.svg
new file mode 100644
index 000000000..f54924ebf
--- /dev/null
+++ b/web/images/altText_done.svg
@@ -0,0 +1,3 @@
+ 
diff --git a/web/l10n_utils.js b/web/l10n_utils.js
index 11663c22f..cc8b7d5c2 100644
--- a/web/l10n_utils.js
+++ b/web/l10n_utils.js
@@ -82,6 +82,7 @@ const DEFAULT_L10N_STRINGS = {
   editor_free_text2_aria_label: "Text Editor",
   editor_ink2_aria_label: "Draw Editor",
   editor_ink_canvas_aria_label: "User-created image",
+  alt_text_button_label: "Alt text",
 };
 if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
   DEFAULT_L10N_STRINGS.print_progress_percent = "{{progress}}%";