> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-accessible-reading-order.md).

# Accessible Reading Order Showcase Demo Code Sample

Easily load accessible PDF and PDF/UA documents and interact with them via screen reader directly in your browse, without server-side dependencies.

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://showcase.apryse.com/accessible-reading-order" class="button primary">Live demo</a>
{% endhint %}

Easily load accessible PDF and PDF/UA documents and interact with them via screen reader directly in your browser, without server-side dependencies.

This demo allows you to:

* Load your accessible PDF or PDF/UA documents
* Interact with document via the document reader
* Advance to the next segment or return to a previous segment

**Implementation steps** To add accessibility via a screen reader to a PDF or PDF/UA with WebViewer:

Step 1: Choose your [preferred web stack](/web/get-started/readme.md) Step 2: Add the ES6 JavaScript sample code provided in this guide

Once you generate your license key, it will automatically be included in your sample code below.

{% @apryse-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

<pre class="language-js" data-line-numbers><code class="lang-js">/* ES6 Compliant Syntax */
/* GitHub Copilot v1.0, Claude 3.5 Sonnet, August 24, 2025 */
/* File: index.js */

import WebViewer from '@pdftron/webviewer';

// Accessible Reading Order section
//
// Code to load accessible PDFs and PDF/UA documents and interact with them
// via screen reader directly in your browser, without server-side
// dependencies
//

const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/PDFUA_AcademicAbstract.pdf';

let isInAccessibleReadingOrderMode = false;
let isAccessibleReadingOrderModeNoStructure = false;
let currentPage = 1;
let prevItem = null;
let currentText = '';
let isMuted = false;

// Reset all variables when a new document is loaded
const onDocumentLoaded = async () => {
    // Reset the demo variables
    isInAccessibleReadingOrderMode = false;
    isAccessibleReadingOrderModeNoStructure = false;
    currentPage = 1;
    prevItem = null;
    currentText = '';
};

// Set accessible reading order mode no structure state
const onAccessibleReadingOrderModeNoStructure = () => {
    isAccessibleReadingOrderModeNoStructure = true;
    console.log('No structure detected in the document.');

    // Update UI controls
    updateUIControls();
};

const onAccessibleReadingOrderModeStarted = () => {
    isInAccessibleReadingOrderMode = true;
    console.log('Accessible reading order mode started.');

    // Update UI controls
    updateUIControls();
};

const onAccessibleReadingOrderModeEnded = () => {
    isInAccessibleReadingOrderMode = false;
    console.log('Accessible reading order mode ended.');

    // Update UI controls
    updateUIControls();
};

const customizeUI = async (instance) => {
    const { documentViewer } = instance.Core;

    // Set the default document to load
    await instance.Core.documentViewer.loadDocument(defaultDoc, {
        extension: 'pdf',
    });

    // Initialize the accessible reading order mode
    const manager = documentViewer.getAccessibleReadingOrderManager();
    await manager.startAccessibleReadingOrderMode();
    isInAccessibleReadingOrderMode = manager.isInAccessibleReadingOrderMode();

    // Add event listeners
    documentViewer.addEventListener('documentLoaded', onDocumentLoaded);
    manager.addEventListener('accessibleReadingOrderModeNoStructure', onAccessibleReadingOrderModeNoStructure);
    manager.addEventListener('accessibleReadingOrderModeStarted', onAccessibleReadingOrderModeStarted);
    manager.addEventListener('accessibleReadingOrderModeEnded', onAccessibleReadingOrderModeEnded);
};

const getFirstElementOnPage = (instance, page) => {
    const allContentElements = Array.from(
        instance.Core.documentViewer.getViewerElement()
            .querySelectorAll(`[data-element^="a11y-reader-content-${page}_"]`)
    );
    return allContentElements.length > 0 ? allContentElements[0] : null;
};

const getAllContentElements = (instance, page) => {
    return Array.from(
        instance.Core.documentViewer.getViewerElement()
            .querySelectorAll(`[data-element^="a11y-reader-content-${page}_"]`)
    );
};

// Toggle mute
const toggleMute = () => {
    const speechSynth = window?.speechSynthesis;
    if (isMuted) {
        speechSynth?.resume();
    } else {
        speechSynth?.pause();
    }
    isMuted = !isMuted;
};

// Highlight the current text element
const highlightCurrentElementAndChangePage = (instance, element) => {
    if (!element) return;
    element.style.border = '2px solid rgba(0, 0, 0, 0.6)';
    element.style.borderRadius = '4px';
    element.style.boxSizing = 'border-box';

    const dataElement = element.getAttribute('data-element') || '';
    let elementPage = -1;

    if (dataElement &#x26;&#x26; dataElement.startsWith('a11y-reader-content-')) {
        // Parse the page number from a11y-reader-content-{page}-{block}
        const parts = dataElement.split('-');
        if (parts.length > 4) {
            // Should have at least 4 parts to extract the page number       
            elementPage = parseInt(parts[3], 10);
        }
    }

    if (elementPage > 0) {
        const currPage = instance?.Core.documentViewer.getCurrentPage();

        if (elementPage !== currPage) {
            instance?.Core.documentViewer.setCurrentPage(elementPage);
        }
    }
};

// Process selected content
const processSelectedContent = (instance, content) => {
    if (!content) return;

    highlightCurrentElementAndChangePage(instance, content);
    const typeDescription = getElementTypeDescription(content);
    prevItem = content;

    const baseText = content.textContent || '';
    const text = typeDescription + baseText;
    currentText = text;

    speechSynthesis.cancel(); // Cancel any ongoing speech synthesis
    readText(text, isMuted);
};

// Clear highlight from the previous element
const clearPreviousHighlight = () => {
    if (prevItem) {
        prevItem.style.border = 'none';
    }
};

// Get the next element in the reading order
const nextElement = (instance) => {
    const currPage = currentPage;

    clearPreviousHighlight();

    let content = null;

    if (prevItem) {
        // Find siblings that have the a11y-reader-content prefix
        const allContentElements = getAllContentElements(instance, currPage);
        const currentIndex = allContentElements.findIndex((el) => el === prevItem);

        // Get the next element if available
        if (currentIndex !== -1 &#x26;&#x26; currentIndex &#x3C; allContentElements.length - 1) {
            content = allContentElements[currentIndex + 1];
        }
    } else {
        content = getFirstElementOnPage(instance, currPage);
    }


    if (!content) {
        content = getFirstElementOnPage(instance, currPage + 1);

        if (content) {
            currentPage += 1;
        } else {
            // Reset to start if no more content
            currentPage = 1;
            content = getFirstElementOnPage(instance, 1);
        }
    }

    if (content) {
        processSelectedContent(instance, content);
    }
};

// Get the previous element in the reading order
const previousElement = (instance) => {
    const currPage = currentPage;

    clearPreviousHighlight();
    let content = null;

    if (prevItem) {
        // Find siblings that have the a11y-reader-content prefix
        const allContentElements = getAllContentElements(instance, currPage);
        const currentIndex = allContentElements.findIndex((el) => el === prevItem);

        // Get the previous element if available
        if (currentIndex > 0) {
            content = allContentElements[currentIndex - 1];
        } else if (currPage > 1) {
            // if we are at the first element of the page, try to get the last element of the previous page
            const prevPageElements = getAllContentElements(instance, currPage - 1);

            if (prevPageElements.length > 0) {
                content = getFirstElementOnPage(instance, currPage - 1);
                currentPage -= 1;
            }
        }
    } else {
        // No current item yet, get the first one
        content = getFirstElementOnPage(instance, currPage);
    }

    // If no content found, reset to the first element of the first page
    if (!content) {
        currentPage = 1;
        content = getFirstElementOnPage(instance, 1);
    }

    if (content) {
        processSelectedContent(instance, content);
    }
};

// Speech Synthesis section
// 
// Code to read the text content of the document using the browser's
// speech synthesis API
//

const voiceIndex = 0;
const pitch = 1;
const rate = 1;
const volume = 1;

const readText = (documentText, isMuted) => {
    const speechSynth = window?.speechSynthesis;
    const utterance = new window.SpeechSynthesisUtterance(documentText);
    if (voiceIndex) {
        utterance.voice = speechSynth?.getVoices()[voiceIndex];
    }
    utterance.pitch = pitch;
    utterance.rate = rate;
    utterance.volume = isMuted ? 0 : volume;

    if (speechSynth?.speaking) {
        speechSynth?.resume();
    } else {
        speechSynth?.speak(utterance);
    }
};

const getElementTypeDescription = (element) => {
    const role = element.getAttribute('role')?.toLowerCase() || '';

    if (role === 'heading') {
        const level = element.getAttribute('aria-level');
        if (level) {
            return `Heading level ${level}, `;
        }
    }

    // Check for common semantic elements based on tag name or class
    const tagName = element.tagName?.toLowerCase() || '';
    if (tagName) {
        if (tagName === 'h1') return 'Heading level 1, ';
        if (tagName === 'h2') return 'Heading level 2, ';
        if (tagName === 'h3') return 'Heading level 3, ';
        if (tagName === 'h4') return 'Heading level 4, ';
        if (tagName === 'h5') return 'Heading level 5, ';
        if (tagName === 'h6') return 'Heading level 6, ';

        if (tagName === 'img' || role === 'img') {
            const alt = element.getAttribute('alt');
            if (alt) return `Image, ${alt}, `;
            return 'Image, ';
        }

        if (tagName === 'figure') return 'Figure, ';
        if (tagName === 'table' || role === 'table') return 'Table, ';
        if (tagName === 'ul' || role === 'list') return 'List, ';
        if (tagName === 'ol') return 'Ordered list, ';
        if (tagName === 'li' || role === 'listitem') return 'List item, ';
        if (tagName === 'a' || role === 'link') return 'Link, ';
        if (tagName === 'button' || role === 'button') return 'Button, ';
        if (tagName === 'input' || role === 'textbox') return 'Input field, ';
    }

    return '';
};


// WebViewer section
// 
// This code initializes the WebViewer with the basic settings
// that are found in the default showcase WebViewer
//

const searchParams = new URLSearchParams(window.location.search);
const history = window.history || window.parent.history || window.top.history;
const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';
const element = document.getElementById('viewer');

// Initialize WebViewer with the specified settings
WebViewer({
    path: '/lib',
    fullAPI: true, // Required for Accessible Reading Order
    licenseKey: licenseKey,
    enableFilePicker: true,
}, element).then((instance) => {
    // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
    const cloudyTools = [
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
    ];
    instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
    instance.UI.disableTools(cloudyTools);

    // Set default toolbar group to Annotate
    instance.UI.setToolbarGroup('toolbarGroup-Annotate');

    // Set default tool on mobile devices to Pan.
    // https://apryse.atlassian.net/browse/WVR-3134
    if (isMobileDevice()) {
        instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
    }

    instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
        if (searchParams.has('file')) {
            searchParams.delete('file');
            history.replaceState(null, '', '?' + searchParams.toString());
        }
    });

    instance.Core.annotationManager.enableAnnotationNumbering();

    instance.UI.NotesPanel.enableAttachmentPreview();

    // Add the demo-specific functionality
    customizeUI(instance).then(() => {
        // Create UI controls after demo is initialized
        createUIControls(instance);
    });
});

// Function to check if the user is on a mobile device
const isMobileDevice = () => {
    return (
        /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
            window.navigator.userAgent
        ) ||
        /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
            window.navigator.userAgent.substring(0, 4)
        )
    );
}

// Cleanup function for when the demo is closed or page is unloaded
const cleanup = (instance) => {
    const { documentViewer } = instance.Core;
    const manager = documentViewer.getAccessibleReadingOrderManager();

    if (typeof instance !== 'undefined' &#x26;&#x26; instance.UI) {

        // Cleanup
        manager.endAccessibleReadingOrderMode();
        speechSynthesis.cancel();

        // Remove event listeners
        manager.removeEventListener('accessibleReadingOrderModeStarted', onAccessibleReadingOrderModeStarted);
        manager.removeEventListener('accessibleReadingOrderModeEnded', onAccessibleReadingOrderModeEnded);
        manager.removeEventListener('accessibleReadingOrderModeNoStructure', onAccessibleReadingOrderModeNoStructure);
        documentViewer.removeEventListener('documentLoaded', onDocumentLoaded);

        console.log('Cleaning up compare-files demo');
    }
};

// Register cleanup for page unload
window.addEventListener('beforeunload', () => cleanup());
window.addEventListener('unload', () => cleanup());

// UI section
// 
// Helper code to add controls to the viewer holding the buttons
// This code creates a container for the buttons, styles them, and adds them to the viewer
//

// Previous button
const prevButton = (instance) => {
    const button = document.createElement('button');
    button.className = 'btn btn-prev';
    button.textContent = 'Previous';
    button.disabled = true; // Initially disabled
    button.onclick = () => {
        previousElement(instance);
        updateUIControls();
    };

    return button;
};

// Next button
const nextButton = (instance) => {
    const button = document.createElement('button');
    button.className = 'btn btn-next';
    button.textContent = 'Next';
    button.disabled = true; // Initially disabled
    button.onclick = () => {
        nextElement(instance);
        updateUIControls();
    };
    
    return button;
};

// Audio button
const audioButton = (instance) => {
    const button = document.createElement('button');
    button.className = 'btn btn-audio';
    button.textContent = 'Mute';
    button.disabled = true; // Initially disabled
    button.onclick = () => {
        toggleMute(instance);
        if (isMuted) {
            button.textContent = 'Unmute';
        } else {
            button.textContent = 'Mute';
        }
    };

    return button;
};

// Untagged Document alert and Current page text
const currentPageText = () => {
    const wrapper = document.createElement('div');
    wrapper.className = 'current-page-text-wrapper';

    const untaggedDocumentAlert = document.createElement('div');
    untaggedDocumentAlert.className = 'untagged-document-alert';

    const untaggedDocumentAlertTitle = document.createElement('span');
    untaggedDocumentAlertTitle.className = 'untagged-document-alert-title';
    untaggedDocumentAlertTitle.textContent = 'UNTAGGED DOCUMENT';

    const untaggedDocumentAlertText = document.createElement('span');
    untaggedDocumentAlertText.className = 'untagged-document-alert-text';
    untaggedDocumentAlertText.textContent = 'No accessible tags or structure detected. Please upload a tagged document for the best accessible experience.';

    untaggedDocumentAlert.appendChild(untaggedDocumentAlertTitle);
    untaggedDocumentAlert.appendChild(untaggedDocumentAlertText);

    wrapper.appendChild(untaggedDocumentAlert);

    if (isAccessibleReadingOrderModeNoStructure) {
        untaggedDocumentAlert.classList.add('visible');
    }

    const currentPageText = document.createElement('span');
    currentPageText.className = 'current-page-text';
    currentPageText.textContent = `Current Page: ${currentPage}`;
    wrapper.appendChild(currentPageText);

    return wrapper;
};

// Text console
const textConsole = () => {
    const wrapper = document.createElement('div');
    wrapper.className = 'text-wrapper';

    const textArea = document.createElement('div');
    textArea.className = 'text-area';

    const textConsoleTitle = document.createElement('div');
    textConsoleTitle.className = 'text-console-title';
    textConsoleTitle.textContent = 'Text Console';

    textArea.appendChild(textConsoleTitle);

    const textContent = document.createElement('div');
    textContent.className = 'text-content';
    textContent.textContent = currentText ? currentText : 'No text is currently being read. Press "Next" to begin reading content.';

    textArea.appendChild(textContent);

    wrapper.appendChild(textArea);

    return wrapper;
};

const createUIControls = (instance) => {
    // Create a container for all controls (label, dropdown, and buttons)
    const controlsContainer = document.createElement('div');
    controlsContainer.className = 'controls-container';

    // Container for Buttons and Page Text
    const leftContainer = document.createElement('div');
    leftContainer.className = 'left-container';

    // Buttons Container
    const buttonsContainer = document.createElement('div');
    buttonsContainer.className = 'buttons-container';
    buttonsContainer.appendChild(prevButton(instance));
    buttonsContainer.appendChild(nextButton(instance));
    buttonsContainer.appendChild(audioButton(instance));
    leftContainer.appendChild(buttonsContainer);

    // Current Page Text
    leftContainer.appendChild(currentPageText());

    controlsContainer.appendChild(leftContainer);

    // Text Console
    const textConsoleElement = textConsole();
    controlsContainer.appendChild(textConsoleElement);

    element.insertBefore(controlsContainer, element.firstChild);

    updateUIControls();
};

const updateUIControls = () => {
    // Update the state of the buttons based on the current mode
    const nextButton = document.querySelector('.btn-next');
    if (nextButton) {
        nextButton.disabled = !isInAccessibleReadingOrderMode;
    }

    const prevButton = document.querySelector('.btn-prev');
    if (prevButton) {
        prevButton.disabled =
            !isInAccessibleReadingOrderMode ||
            !prevItem;
    }

    const audioButton = document.querySelector('.btn-audio');
    if (audioButton) {
        audioButton.disabled = !isInAccessibleReadingOrderMode;
    }

    // Show or hide the untagged document alert
    const untaggedDocumentAlert = document.querySelector('.untagged-document-alert');
    if (untaggedDocumentAlert) {
        if (isAccessibleReadingOrderModeNoStructure) {
            untaggedDocumentAlert.classList.add('visible');
        } else {
            untaggedDocumentAlert.classList.remove('visible');
        }
    }

    // Update the current page text
    const currentPageText = document.querySelector('.current-page-text');
    if (currentPageText) {
        currentPageText.textContent = `Current Page: ${currentPage}`;
    }

    // Update the text content in the text console
    const textContent = document.querySelector('.text-content');
    if (textContent) {
        textContent.textContent = currentText ? currentText : 'No text is currently being read. Press "Next" to begin reading content.';
    }
}
</code></pre>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-accessible-reading-order.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
