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:
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) {
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 (
height: '4px',
backgroundColor: 'rgb(53, 126, 221)',
height: '100%',
width: `${percentages}%`,

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();

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 = React.useCallback(
(Toolbar: (props: ToolbarProps) => React.ReactElement) => (
<Toolbar />
bottom: '-0.25rem',
position: 'absolute',
left: 0,
// Take the full width of toolbar
width: '100%',
<ReadingIndicator />
const defaultLayoutPluginInstance = defaultLayoutPlugin({
Last but not least, in order to make the reading progress bar displayed right below the toolbar container, we use a negative value for the `bottom` property as you see above.
Scroll in the pages container to see the reading progress bar (The sample code)

See also