This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/react-pdf-text-layer-custom-renderer.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. react-pdf text layer: Selection, search, and redaction

Table of contents

    A guide to the react-pdf text layer — enabling text selection and copy-paste, building custom text renderers for search highlighting and visual masking, and extracting raw text content from PDF pages.
    react-pdf text layer: Selection, search, and redaction
    TL;DR
    • The text layer is enabled by default — import react-pdf/dist/Page/TextLayer.css and you get native selection, copy-paste, and Control-F search for free.
    • customTextRenderer({ str, itemIndex }) lets you wrap or replace text per item. Its return value is inserted as HTML, so escape any untrusted input before concatenating it into tags.
    • onGetTextSuccess({ items, styles }) gives you raw text and font metadata — pair it with renderMode="none" and renderTextLayer={false} for headless extraction.
    • customTextRenderer produces visual masking, not real redaction. The underlying text stays in the source PDF and in onGetTextSuccess. For true redaction, modify the PDF bytes (e.g. with pdf-lib) or use an SDK with a redaction API.

    The text layer is an invisible HTML overlay on top of the canvas that enables text selection, copy-paste, and accessibility. react-pdf also supports a custom text renderer for highlighting or transforming text.

    Security note: customTextRenderer returns a string that react-pdf injects as HTML, not text. If the PDF (or any user-controlled input you concatenate into the return value) contains characters like <, >, or &, the browser will parse them as markup. Always HTML-escape str and any user input before wrapping it in tags — the examples below use an escapeHtml helper for this.

    Enabling the text layer

    The text layer is enabled by default. Make sure you import the CSS:

    import "react-pdf/dist/Page/TextLayer.css";
    <Page pageNumber={1} renderTextLayer={true} /> {/* default */}

    To disable the text layer, set renderTextLayer to false:

    <Page pageNumber={1} renderTextLayer={false} />

    What the text layer does

    • Overlays invisible <span> elements on top of the canvas, positioned to match the rendered text
    • Enables native text selection (click and drag)
    • Enables copy-paste (Control-C/Command-C)
    • Enables the browser’s built-in find (Control-F/Command-F)
    • Provides accessibility for screen readers

    Accessing text content

    Use the onGetTextSuccess callback to access the raw text data:

    <Page
    pageNumber={1}
    onGetTextSuccess={({ items, styles }) => {
    // `items` is an array of text items.
    items.forEach((item) => {
    console.log(item.str); // The text string.
    console.log(item.dir); // Text direction ("ltr" or "rtl").
    console.log(item.width); // Width in PDF points.
    console.log(item.height); // Height in PDF points.
    console.log(item.transform); // Position/rotation matrix.
    console.log(item.hasEOL); // Followed by line break?
    });
    // `styles` contains font information keyed by font name.
    console.log(styles);
    }}
    />

    Custom text renderer

    The customTextRenderer prop lets you modify how text items are rendered. It receives each text item and returns a string (which can contain HTML):

    <Page
    pageNumber={1}
    customTextRenderer={({ str, itemIndex }) => {
    // `str`: the text string for this item.
    // `itemIndex`: index of the item in the text content array.
    return str;
    }}
    />

    Example: Highlighting search terms

    This component wraps every match for searchText in a <mark> tag so it renders highlighted in the text layer:

    function HighlightedPage({ pageNumber, searchText }) {
    const customTextRenderer = useCallback(
    ({ str }) => {
    const escaped = escapeHtml(str);
    if (!searchText) return escaped;
    const regex = new RegExp(`(${escapeRegex(searchText)})`, "gi");
    // `$1` is the matched substring from `escaped`, so it's already HTML-safe.
    return escaped.replace(regex, '<mark class="highlight">$1</mark>');
    },
    [searchText],
    );
    return (
    <Page
    pageNumber={pageNumber}
    customTextRenderer={customTextRenderer}
    />
    );
    }
    function escapeHtml(s) {
    return s.replace(/[&<>"']/g, (c) => ({
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;",
    })[c]);
    }
    function escapeRegex(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    Note that escapeRegex covers regex metacharacters and escapeHtml covers HTML special characters — you need both. escapeRegex alone leaves you open to HTML injection if the PDF text contains tags.

    .highlight {
    background-color: yellow;
    color: black;
    border-radius: 2px;
    padding: 0 1px;
    }

    Example: Visually masking sensitive content

    This is visual masking, not redaction. customTextRenderer only changes how text appears in the rendered text layer — the original text remains in the PDF file, in onGetTextSuccess, and in any download or print of the original document. For true redaction, you need to rewrite the PDF bytes (e.g. with pdf-lib(opens in a new tab) or Nutrient Web SDK’s redaction API).

    function MaskedPage({ pageNumber, maskPatterns }) {
    const customTextRenderer = useCallback(
    ({ str }) => {
    let result = escapeHtml(str);
    for (const pattern of maskPatterns) {
    // Recreate the pattern with the same flags so it operates on the
    // HTML-escaped string. Patterns should target characters that
    // can't be affected by HTML escaping (digits, letters, etc.).
    result = result.replace(pattern, (match) =>
    `<span class="redacted">${"\u2588".repeat(match.length)}</span>`,
    );
    }
    return result;
    },
    [maskPatterns],
    );
    return (
    <Page
    pageNumber={pageNumber}
    customTextRenderer={customTextRenderer}
    />
    );
    }

    Example: Adding tooltips

    This renderer wraps any email address it finds in a <span> with a tooltip:

    const customTextRenderer = ({ str }) => {
    // Escape first, then wrap. The capture group `$1` is safe because the
    // email regex only matches characters that survive HTML escaping unchanged.
    return escapeHtml(str).replace(
    /(\S+@\S+\.\S+)/g,
    '<span title="Click to email" class="email-link">$1</span>',
    );
    };

    Extracting all text from a PDF

    This component mounts every page with rendering disabled and collects each page’s text as onGetTextSuccess fires:

    function TextExtractor({ file }) {
    const [allText, setAllText] = useState([]);
    const handleTextSuccess = useCallback(
    (pageNumber) =>
    ({ items }) => {
    const pageText = items.map((item) => item.str).join(" ");
    setAllText((prev) => {
    const updated = [...prev];
    updated[pageNumber - 1] = pageText;
    return updated;
    });
    },
    [],
    );
    return (
    <Document file={file} onLoadSuccess={({ numPages }) => setAllText(new Array(numPages).fill(""))}>
    {allText.map((_, i) => (
    <Page
    key={i}
    pageNumber={i + 1}
    onGetTextSuccess={handleTextSuccess(i + 1)}
    renderMode="none" // Don't render canvas.
    renderTextLayer={false} // Don't render text layer DOM.
    />
    ))}
    </Document>
    );
    }

    Text layer callbacks

    Page also exposes success and error callbacks for both text extraction and text layer rendering:

    <Page
    pageNumber={1}
    onGetTextSuccess={({ items, styles }) => {
    // Text content extracted from page.
    }}
    onGetTextError={(error) => {
    // Failed to extract text.
    }}
    onRenderTextLayerSuccess={() => {
    // Text layer DOM rendered.
    }}
    onRenderTextLayerError={(error) => {
    // Failed to render text layer.
    }}
    />

    Key points

    • Import react-pdf/dist/Page/TextLayer.css or text selection won’t work.
    • renderTextLayer is true by default.
    • customTextRenderer receives { str, itemIndex } and returns a string (can include HTML).
    • Use customTextRenderer for search highlighting, redaction, or text transformation.
    • onGetTextSuccess gives you raw text data for extraction/indexing.
    • renderMode="none" with renderTextLayer={false} is useful for text-only extraction without visual rendering.

    How Nutrient Web SDK handles this

    Instead of building custom text renderers with regex replacement and manual CSS, Nutrient Web SDK provides text selection and search out of the box:

    // Built-in text selection + full-featured search.
    const results = await instance.search("search term");
    instance.setSearchState((state) => state.set("results", results));
    // Text extraction via API.
    const pageText = await instance.textLinesForPageIndex(0);

    There’s no customTextRenderer with regex replacement, and no manual CSS for highlights. Nutrient provides a full-featured search UI with match counts, result navigation, case sensitivity, and whole-word matching — plus programmatic text extraction without rendering.

    Learn more about Nutrient Web SDK | Migration guide | Contact Sales

    FAQ

    Why is my text selection blank or misaligned?

    You’re probably missing the text layer stylesheet. Import react-pdf/dist/Page/TextLayer.css before rendering any <Page>. Without it, the invisible text spans don’t position correctly over the canvas and selections render in the wrong place or invisibly.

    Is customTextRenderer safe against XSS?

    Only if you escape its return value — otherwise it’s vulnerable to cross-site scripting (XSS). react-pdf injects the returned string as HTML, so any unescaped <, >, or & in the PDF text (or in user input you concatenate in) will be parsed as markup. Use the escapeHtml helper shown above before adding tags.

    Can I use customTextRenderer to redact sensitive data?

    Not safely. It only changes how text renders — the original text remains in the source PDF, in onGetTextSuccess, and in any export of the file. For real redaction, rewrite the PDF bytes with a library like pdf-lib, or use an SDK that has a dedicated redaction API.

    How do I extract text from every page without rendering the canvas?

    Mount each <Page> with renderMode="none" and renderTextLayer={false}, and listen for onGetTextSuccess. PDF.js still loads the page object and runs text extraction, but it skips the canvas paint and DOM text layer, resulting in much lower memory and CPU usage for headless extraction.

    Does customTextRenderer rerun on every render?

    It reruns whenever the function identity changes. Wrap it in useCallback keyed on the values it closes over (search term, redact patterns, etc.). Otherwise, react-pdf rerenders the text layer on every parent render, which is wasteful and can cause flicker.

    What’s in the styles object from onGetTextSuccess?

    styles is a map keyed by font name with per-font metadata, including fontFamily, ascent, descent, and vertical. PDF.js uses this internally to position text spans; you can use it for things like font analysis or matching PDF styles in your own UI overlays.

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?