> 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/webviewer-custom-ui.md).

# Custom UI Enhancements to WebViewer with React

Sample that demonstrates several features to customize WebViewer into a React app that renders without \<iFrame>, \<button> elements for expanded functions, and on leveraging search APIs.

This sample demonstrates several features to customize WebViewer into a React app. The UI customization includes:

* How to leverage Apryse's document renderer without an `<iFrame>`
* How to define custom `<button>` elements and implement functionality from the Apryse SDK such as
  * Zoom In/Out
  * Drawing Rectangles
  * Select Tool
  * Creating and Applying Redactions
* How to implement searching using [DocViewer Search APIs](/web/search/advance-text-search.md)

WebViewer provides a slick out-of-the-box responsive UI that enables you to view, annotate and manipulate PDFs and other document types inside any web project. Check out [getting started with WebViewer for React](/web/get-started/libraries-and-frameworks/react.md).

Click the button below to view the full project in GitHub.

{% tabs %}
{% tab title="App.js" %}
{% code title="App.js" lineNumbers="true" %}

```js
import React, { useRef, useEffect, useState } from 'react';
import SearchContainer from './components/SearchContainer';
import { ReactComponent as ZoomIn } from './assets/icons/ic_zoom_in_black_24px.svg';
import { ReactComponent as ZoomOut } from './assets/icons/ic_zoom_out_black_24px.svg';
import { ReactComponent as AnnotationRectangle } from './assets/icons/ic_annotation_square_black_24px.svg';
import { ReactComponent as AnnotationRedact } from './assets/icons/ic_annotation_add_redact_black_24px.svg';
import { ReactComponent as AnnotationApplyRedact} from './assets/icons/ic_annotation_apply_redact_black_24px.svg';
import { ReactComponent as Search } from './assets/icons/ic_search_black_24px.svg';
import { ReactComponent as Select } from './assets/icons/ic_select_black_24px.svg';
import { ReactComponent as EditContent } from './assets/icons/ic_edit_page_24px.svg';
import { ReactComponent as AddParagraph } from './assets/icons/ic_paragraph_24px.svg';
import { ReactComponent as AddImageContent } from './assets/icons/ic_add_image_24px.svg';
import './App.css';

/* global globalThis */
const root = globalThis;

const App = () => {
  const viewer = useRef(null);
  const scrollView = useRef(null);
  const searchTerm = useRef(null);
  const searchContainerRef = useRef(null);

  const [documentViewer, setDocumentViewer] = useState(null);
  const [annotationManager, setAnnotationManager] = useState(null);
  const [searchContainerOpen, setSearchContainerOpen] = useState(false);
  const [isInContentEditMode, setIsInContentEditMode] = useState(false);

  const Annotations = root.Core?.Annotations;

  // if using a class, equivalent of componentDidMount
  useEffect(() => {
    let pollTimer;

    const initViewer = () => {
      const Core = root.Core;
      if (!Core) {
        pollTimer = setTimeout(initViewer, 50);
        return;
      }

      Core.setWorkerPath('/webviewer');
      Core.enableFullPDF();

      const documentViewer = new Core.DocumentViewer();
      documentViewer.setScrollViewElement(scrollView.current);
      documentViewer.setViewerElement(viewer.current);
      documentViewer.enableAnnotations();
      documentViewer.loadDocument('/files/demo.pdf');

      setDocumentViewer(documentViewer);

      documentViewer.addEventListener('documentLoaded', () => {
        console.log('document loaded');
        documentViewer.setToolMode(documentViewer.getTool(Core.Tools.ToolNames.EDIT));
        setAnnotationManager(documentViewer.getAnnotationManager());
      });
    };

    initViewer();

    return () => {
      if (pollTimer) {
        clearTimeout(pollTimer);
      }
    };
  }, []);

  const zoomOut = () => {
    documentViewer.zoomTo(documentViewer.getZoomLevel() - 0.25);
  };

  const zoomIn = () => {
    documentViewer.zoomTo(documentViewer.getZoomLevel() + 0.25);
  };

  const startEditingContent = () => {
    const contentEditManager = documentViewer.getContentEditManager();
    contentEditManager.startContentEditMode();
    setIsInContentEditMode(true);
  }

  const endEditingContent = () => {
    setIsInContentEditMode(false);
    documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.EDIT));
    const contentEditManager = documentViewer.getContentEditManager();
    contentEditManager.endContentEditMode();
  }

  const addParagraph = () => {
    if (isInContentEditMode) {
      const addParagraphTool = documentViewer.getTool(root.Core.Tools.ToolNames.ADD_PARAGRAPH);
      documentViewer.setToolMode(addParagraphTool);
    } else {
      alert('Content Edit mode is not enabled.')
    }
  };

  const addImageContent = () => {
    if (isInContentEditMode) {
      const addImageContentTool = documentViewer.getTool(root.Core.Tools.ToolNames.ADD_IMAGE_CONTENT);
      documentViewer.setToolMode(addImageContentTool);
    } else {
      alert('Content Edit mode is not enabled.')
    }
  };

  const createRectangle = () => {
    documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.RECTANGLE));
  };

  const selectTool = () => {
    documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.EDIT));
  };

  const createRedaction = () => {
    documentViewer.setToolMode(documentViewer.getTool(root.Core.Tools.ToolNames.REDACTION));
  };

  const applyRedactions = async () => {
    const annotationManager = documentViewer.getAnnotationManager();
    annotationManager.enableRedaction();
    await annotationManager.applyRedactions();
  };

  return (
    <div className="App">
      <div id="main-column">
        <div className="center" id="tools">
          <button onClick={zoomOut}>
            <ZoomOut />
          </button>
          <button onClick={zoomIn}>
            <ZoomIn />
          </button>
          <button onClick={startEditingContent} title="Switch to edit mode">
            <EditContent />
          </button>
          <button onClick={addParagraph} title="Add new paragraph">
            <AddParagraph />
          </button>
          <button onClick={addImageContent} title="Add new content image">
            <AddImageContent />
          </button>
          <button onClick={endEditingContent} title="End edit mode">
            Finish Editing
          </button>
          <button onClick={createRectangle}>
            <AnnotationRectangle />
          </button>
          <button onClick={createRedaction} title="Create Redaction">
            <AnnotationRedact />
          </button>
          <button onClick={applyRedactions} title="Apply Redactions">
            <AnnotationApplyRedact />
          </button>
          <button onClick={selectTool}>
            <Select />
          </button>
          <button
            onClick={() => {
              // Flip the boolean
              setSearchContainerOpen(prevState => !prevState);
            }}
          >
            <Search />
          </button>
        </div>
        <div className="flexbox-container" id="scroll-view" ref={scrollView}>
          <div id="viewer" ref={viewer}></div>
        </div>
      </div>
      <div className="flexbox-container">
        <SearchContainer
          Annotations={Annotations}
          annotationManager={annotationManager}
          documentViewer={documentViewer}
          searchTermRef={searchTerm}
          searchContainerRef={searchContainerRef}
          open={searchContainerOpen}
        />
      </div>
    </div>
  );
};

export default App;

```

{% endcode %}
{% endtab %}

{% tab title="SearchContainer.js" %}
{% code title="SearchContainer.js" lineNumbers="true" %}

```js
import React, { useState, useEffect } from 'react';
import ClearSearch from '../../assets/icons/ic_close_black_24px.svg'
import LeftChevronArrow from '../../assets/icons/ic_chevron_left_black_24px.svg'
import RightChevronArrow from '../../assets/icons/ic_chevron_right_black_24px.svg'
import Search from '../../assets/icons/ic_search_black_24px.svg'
import './SearchContainer.css';

const SearchContainer = (props) => {
  const [searchResults, setSearchResults] = useState([]);
  const [activeResultIndex, setActiveResultIndex] = useState(-1);
  const [toggledSearchModes, setToggledSearchModes] = useState([]);

  const {
    annotationManager,
    documentViewer,
    open = false,
    searchContainerRef,
    searchTermRef: searchTerm,
  } = props;

  const pageRenderTracker = {};

  /**
   * Coupled with the function `changeActiveSearchResult`
   */
  useEffect(() => {
    if (activeResultIndex >= 0 && activeResultIndex < searchResults.length) {
      documentViewer.setActiveSearchResult(searchResults[activeResultIndex]);
    }
  }, [ activeResultIndex ]);

  /**
   * Side-effect function that invokes `documentViewer.textSearchInit`, and stores
   * every result in the state Array `searchResults`, and jumps the user to the
   * first result is found.
   */
  const performSearch = () => {
    clearSearchResults(false);

    if (!documentViewer || !window.Core?.Search) {
      return;
    }

    const {
      current: {
        value: textToSearch
      }
    } = searchTerm;

    const {
      PAGE_STOP,
      HIGHLIGHT,
      AMBIENT_STRING
    } = window.Core.Search.Mode;

    const mode = toggledSearchModes.reduce(
      (prev, value) => prev | value,
      (PAGE_STOP | HIGHLIGHT | AMBIENT_STRING),
    );
    const fullSearch = true;
    let jumped = false;
    documentViewer.textSearchInit(textToSearch, mode, {
      fullSearch,
      onResult: (result) => {
        const foundCode =
          window.Core?.Search?.ResultCode?.FOUND ??
          window.PDFNet?.TextSearch?.ResultCode?.FOUND;

        if (result.resultCode !== foundCode) {
          return;
        }

        setSearchResults((prevState) => [...prevState, result]);

        if (!jumped) {
          jumped = true;
          const pageNumber = result.pageNum ?? result.page_number;
          // This is the first result found, so set `activeResult` accordingly
          setActiveResultIndex(0);
          documentViewer.displaySearchResult(result, () => {
            /**
             * The page number in documentViewer.displayPageLocation is not
             * 0-indexed
             */
            documentViewer.displayPageLocation(pageNumber, 0, 0, true);
          });
        }
      },
    });
  };

  /**
   * Side-effect function that invokes the internal functions to clear the
   * search results
   *
   * @param {Boolean} clearSearchTermValue For the guard clause to determine
   * if `searchTerm.current.value` should be mutated (would not want this to
   * occur in the case where a subsequent search is being performed after a
   * previous search)
   */
  const clearSearchResults = (clearSearchTermValue = true) => {
    if (clearSearchTermValue) {
      searchTerm.current.value = '';
    }
    documentViewer.clearSearchResults();
    annotationManager.deleteAnnotations(annotationManager.getAnnotationsList());
    setSearchResults([]);
    setActiveResultIndex(-1);
  };

  /**
   * Checks if the key that has been released was the `Enter` key, and invokes
   * `performSearch` if so
   *
   * @param {SyntheticEvent} event The event passed from the `input` element
   * upon the function being invoked from a listener attribute, such as
   * `onKeyUp`
   */
  const listenForEnter = (event) => {
    const {
      keyCode,
    } = event;
    // The key code for the enter button
    if (keyCode === 13) {
      // Cancel the default action, if needed
      event.preventDefault();
      // Trigger the button element with a click
      performSearch();
    }
  };

  /**
   * Changes the active search result in `documentViewer`
   *
   * @param {Number} newSearchResult The index to set `activeResult` to,
   * indicating which `result` object that should be passed to
   * `documentViewer.setActiveSearchResult`
   */
  const changeActiveSearchResult = (newSearchResult) => {
    /**
     * @todo Figure out why only the middle set of search results can be
     * iterated through, but not the first or last results.
     */
    /**
     * Do not try to set a search result that is outside of the index range of
     * searchResults
     */
    if (newSearchResult >= 0 && newSearchResult < searchResults.length) {
      setActiveResultIndex(newSearchResult);
    }
  };

  /**
   * Toggles the given `searchMode` value within `toggledSearchModes`
   *
   * @param {CoreControls.DocumentViewer.SearchMode} searchMode The bitwise
   * search mode value to toggle on or off
   */
  const toggleSearchMode = (searchMode) => {
    if (!toggledSearchModes.includes(searchMode)) {
      setToggledSearchModes(prevState => [...prevState, searchMode])
    } else {
      setToggledSearchModes(
        prevState => prevState.filter(value => value !== searchMode)
      )
    }
  }

  /**
   * Side-effect function that toggles whether or not to perform a text search
   * with case sensitivty
   */
  const toggleCaseSensitive = () => {
    toggleSearchMode(window.Core.Search.Mode.CASE_SENSITIVE);
  }

  /**
   * Side-effect function that toggles whether or not to perform a text search
   * that finds the whole word
   */
  const toggleWholeWord = () => {
    toggleSearchMode(window.Core.Search.Mode.WHOLE_WORD);
  }

  if (!open) {
    return (null);
  }

  return (
    <span
      id="search-container"
      ref={searchContainerRef}
    >
      <div id="search-input">
        <input
          ref={searchTerm}
          type={'text'}
          placeholder={'Search'}
          onKeyUp={listenForEnter}
        />
        <button onClick={performSearch}>
          <img src={Search} alt="Search"/>
        </button>
      </div>
      <div>
        <span>
          <input
            type="checkbox"
            value={toggledSearchModes.includes(window.Core.Search.Mode.CASE_SENSITIVE)}
            onChange={toggleCaseSensitive}
          />
          Case sensitive
        </span>
        <span>
          <input
            type="checkbox"
            value={toggledSearchModes.includes(window.Core.Search.Mode.WHOLE_WORD)}
            onChange={toggleWholeWord}
          />
          Whole word
        </span>
      </div>
      <div className="divider"></div>
      <div id='search-buttons'>
        <span>
          <button onClick={clearSearchResults}>
            <img src={ClearSearch} alt="Clear Search"/>
          </button>
        </span>
        <span id="search-iterators">
          <button
            onClick={() => { changeActiveSearchResult(activeResultIndex - 1); }}
            disabled={activeResultIndex < 0}
          >
            <img src={LeftChevronArrow} alt="Previous Search Result"/>
          </button>
          <button
            onClick={() => { changeActiveSearchResult(activeResultIndex + 1); }}
            disabled={activeResultIndex < 0}
          >
            <img src={RightChevronArrow} alt="Next Search Result"/>
          </button>
        </span>
      </div>
      <div>
        {
          searchResults.map((result, idx) => {
            const {
              ambient_str: ambientStr,
              page_num: pageNum,
              result_str_start: resultStrStart,
              result_str_end: resultStrEnd,
            } = result;
            const textBeforeSearchValue = ambientStr.slice(0, resultStrStart);
            const searchValue = ambientStr.slice(
              resultStrStart,
              resultStrEnd,
            );
            const textAfterSearchValue = ambientStr.slice(resultStrEnd);
            let pageHeader = null;
            if (!pageRenderTracker[pageNum]) {
              pageRenderTracker[pageNum] = true;
              pageHeader = <div>Page {pageNum}</div>
            }
            return (
              <div key={`search-result-${idx}`} >
                {pageHeader}
                <div
                  className='search-result'
                  onClick={() => {documentViewer.setActiveSearchResult(result)}}
                >
                  {textBeforeSearchValue}
                  <span className="search-value">
                    {searchValue}
                  </span>
                  {textAfterSearchValue}
                </div>
              </div>
            )
          })
        }
      </div>
    </span>
  );
};

export default SearchContainer;

```

{% endcode %}
{% endtab %}
{% endtabs %}


---

# 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/webviewer-custom-ui.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.
