Example: Display reading progress at the top

In this example, we will display a progress bar indicating where the current scroll position is and how long to reach the end of document. The progress bar will be displayed under the toolbar created by the Default Layout plugin.

To do that, we need to handle the scroll event of the element that contains all pages.

Create a plugin

The plugin will serve two things exactly:

  • Access to the container of all pages
  • Provide a component named ReadingIndicator to display the reading progress bar

A plugin that extends from Plugin is able to retrieve the container of all pages via the install function:

import * as React from 'react';
import { createStore, Plugin, PluginFunctions } from '@react-pdf-viewer/core';

interface StoreProps {
    getPagesContainer?(): HTMLElement;
}

interface ReadingIndicatorPlugin extends Plugin {
    ReadingIndicator: () => React.ReactElement;
}

const readingIndicatorPlugin = (): ReadingIndicatorPlugin => {
    const store = React.useMemo(() => createStore<StoreProps>({}), []);

    const ReadingIndicatorDecorator = () => <ReadingIndicator store={store} />;

    return {
        install: (pluginFunctions: PluginFunctions) => {
            store.update('getPagesContainer', pluginFunctions.getPagesContainer);
        },
        ReadingIndicator: ReadingIndicatorDecorator,
    };
};

export default readingIndicatorPlugin;

Usually, the store is a central place to track the internal states of a plugin. Different components of plugin can communicate to each other and the main Viewer component via store.

As you see in the sample code above, the ReadingIndicator component accepts the store instance. From there, we can access the container of all pages, and handle its scroll event:

const ReadingIndicator = ({ store }) => {
    const handleScroll = (e: Event) => {
        // We will implement in later
    };

    const handlePagesContainer = () => {
        const getPagesContainer = store.get('getPagesContainer');
        if (!getPagesContainer) {
            return;
        }

        const pagesEle = getPagesContainer();
        pagesEle.addEventListener('scroll', handleScroll);
    };

    React.useLayoutEffect(() => {
        store.subscribe('getPagesContainer', handlePagesContainer);

        return () => store.unsubscribe('getPagesContainer', handlePagesContainer);
    }, []);
};

It is NOT recomended to handle the scroll event without debouncing the handler. An expensive handler can slow down the application. It is up to you to implement it yourself or choose from popular libraries such as lodash's debounce or Underscore's debounce.

It is quite easy to calculate the current scroll position and how many percentages the user has been scrolling in total of the height of container:

const ReadingIndicator = ({ store }) => {
    const [percentages, setPercentages] = React.useState(0);

    const handleScroll = (e: Event) => {
        const target = e.target;
        if (target instanceof HTMLDivElement) {
            const p = Math.floor(100 * target.scrollTop / (target.scrollHeight - target.clientHeight));
            setPercentages(Math.min(100, p));
        }
    };
};

The internal state percentages indicates how far the user has been scrolling:

const ReadingIndicator = ({ store }) => {
    return (
        <div
            style={{
                height: '4px',
            }}
        >
            <div
                style={{
                    backgroundColor: 'rgb(53, 126, 221)',
                    height: '100%',
                    width: `${percentages}%`,
                }}
            />
        </div>
    );
};

Register the plugin

Like any built-in plugins, we can create a new instance of a given plugin and register it with the main Viewer component:

const readingIndicatorPluginInstance = readingIndicatorPlugin();

<Viewer
    fileUrl={...}
    plugins={[
        readingIndicatorPluginInstance,
        ...
    ]}
/>

Display the reading progress bar

The reading progress bar can be taken from the instance of plugin created in the previous section:

const { ReadingIndicator } = readingIndicatorPluginInstance;

Now we want to put it right below the default toolbar. Fortunately, the Default Layout plugin provides the ability of showing a custom element in the toolbar.

The following code demonstrates how we can customize the toolbar:

import { defaultLayoutPlugin, ToolbarProps } from '@react-pdf-viewer/default-layout';

const renderToolbar = (Toolbar: ((props: ToolbarProps) => React.ReactElement)) => (
    <>
        <Toolbar />
        <div style={{ margin: '4px -4px -4px -4px' }}>
            <ReadingIndicator />
        </div>
    </>
);

const defaultLayoutPluginInstance = defaultLayoutPlugin({
    renderToolbar,
});

In order to make the reading progress bar fit within the toolbar container, we use negative values for the margin property as you see above.

Scroll in the pages container to see the reading progress bar (The sample code)

Related examples