Not long ago, I was looking for an easy way to embed a PDF in my website. I tried many different PDF viewers, but implementing it was a real headache.
You might be in a similar situation. You are trying to add a PDF viewer to your project and want the viewer styled with the same UI framework as you are currently using (in this case MUI), but you have no idea how to do it!
Don't look further! In this article I will exactly tell you how to do that. By the end of the article you will have a PDF viewer working in React Material UI with pagination, zooming, pinch-zoom, page-rotation and different page layouts.
It will look like this:
This is something that would normally take you weeks to build. But by reading this 10-minute article you will have a fully working PDF viewer and you can move on to your next task.
Let's get started.
Installation and Setup
I assume that you already have a MUI React project set up, so I will not go into details on how to do that.
In this article we will be using MUI icons, make sure that you have installed @mui/icons-material
# For PNPM use:
pnpm add @mui/icons-material
# For Yarn use:
yarn add @mui/icons-material
# For NPM use:
npm install @mui/icons-material
We are using an open-source project called Embed PDF which basically allows you to easily Embed a PDF inside your website. In this case we are using their headless option which gives us full control over the UI.
Install the following plugins
# For PNPM use:
pnpm add @embedpdf/core @embedpdf/engines @embedpdf/plugin-loader @embedpdf/plugin-viewport @embedpdf/plugin-scroll @embedpdf/plugin-zoom @embedpdf/plugin-render @embedpdf/plugin-tiling @embedpdf/plugin-rotate @embedpdf/plugin-spread @embedpdf/plugin-fullscreen @embedpdf/plugin-export @embedpdf/plugin-interaction-manager
# For Yarn use:
yarn add @embedpdf/core @embedpdf/engines @embedpdf/plugin-loader @embedpdf/plugin-viewport @embedpdf/plugin-scroll @embedpdf/plugin-zoom @embedpdf/plugin-render @embedpdf/plugin-tiling @embedpdf/plugin-rotate @embedpdf/plugin-spread @embedpdf/plugin-fullscreen @embedpdf/plugin-export @embedpdf/plugin-interaction-manager
# For NPM use:
npm install @embedpdf/core @embedpdf/engines @embedpdf/plugin-loader @embedpdf/plugin-viewport @embedpdf/plugin-scroll @embedpdf/plugin-zoom @embedpdf/plugin-render @embedpdf/plugin-tiling @embedpdf/plugin-rotate @embedpdf/plugin-spread @embedpdf/plugin-fullscreen @embedpdf/plugin-export @embedpdf/plugin-interaction-manager
Great, if the packages are installed it's time to initialize the PDF engine.
I am using PDFium as our PDF engine, which is open-source and maintained by Google. It is the default PDF engine used in Chrome. I chose PDFium because it's battle tested and actively maintained.
We can initialze the PDF engine hook as follows:
import { usePdfiumEngine } from '@embedpdf/engines/react';
const { engine, isLoading, error } = usePdfiumEngine({
wasmUrl: 'https://cdn.jsdelivr.net/npm/@embedpdf/pdfium/dist/pdfium.wasm',
});
Let me show you are simple component that loads the engine inside the EmbedPDF core
import { EmbedPDF } from "@embedpdf/core/react";
import { usePdfiumEngine } from "@embedpdf/engines/react";
import { Box, CircularProgress, Alert } from "@mui/material";
const plugins = [];
const Loading = () => {
return (
<Box
sx={{
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<CircularProgress size={48} />
</Box>
);
};
const PdfViewer = () => {
const { engine, isLoading, error } = usePdfiumEngine({
wasmUrl: "https://cdn.jsdelivr.net/npm/@embedpdf/pdfium/dist/pdfium.wasm",
});
if (error) {
return (
<Alert severity="error">
Failed to initialize PDF viewer: {error.message}
</Alert>
);
}
if (isLoading || !engine) {
return <Loading />;
}
return (
<EmbedPDF onInitialized={async () => {}} engine={engine} plugins={plugins}>
{({ pluginsReady }) => (
<>
{pluginsReady ? (
<Alert severity="success">Engine loaded and core ready</Alert>
) : (
<Loading />
)}
</>
)}
</EmbedPDF>
);
};
export default PdfViewer;
If all set up properly we should see a message "Engine loaded and core ready" as you can see in the sandbox below:
Great the setup is done now! In the next part of this article we are going to setup a very basic headless viewer with a simple MUI toolbar.
Creating the PDF Viewer
The Embed PDF library works with plugins. We will choose for the sake of this article the most common plugins.
We will load the plugins in the array that we created already in the snippet before.
const plugins = [];
I will first give you a short summary of the plugins that we are using. Then we will initialize/install the plugins.
@embedpdf/viewport
This plugin does manages display area, scrolling and viewport metrics
@embedpdf/plugin-scroll
This plugin is for virtualized scrolling. This means that if you have a huge PDF of 1000 pages it will not load all pages because that would probably crash your browser, it only loads that pages that are in your viewport with a bit of margin.
@embedpdf/plugin-zoom
Gives the ability to zoom-in and out on a PDF page. They have also the option to add pinch zoom for mobile and marquee zoom (basically selecting an area of the page and zooming into that)
@embedpdf/plugin-rotate
This plugin allows you to rotate backwards or forwards of the pages in the viewport. This is not for individually pages but for all pages in the viewport
@embedpdf/plugin-spread
This plugin gives you 3 common page-layouts for PDFs, single page, odd pages, even pages
@embedpdf/plugin-export
Ability to download the PDF
@embedpdf/plugin-fullscreen
Speaks for itself. Change the viewer to fullscreen.
@embedpdf/plugin-loader
This is the plugin that loads the PDF, can be a buffer on can be an URL. In our case we are showing an example with a PDF url
@embedpdf/plugin-tiling
The tiling plugin renders the PDF page. But it renders it in tiles. Which gives much better performance for higher zoom levels. If you are zoomed in it will not render the full page, it will only render the tiles that are in the viewport.
Ok those are the plugins we are going to use for our PDF viewer. Let's initialize these plugins.
// Add the lines below the rest of the imports stay the same
import { createPluginRegistration } from '@embedpdf/core';
import { LoaderPluginPackage } from "@embedpdf/plugin-loader";
import { ViewportPluginPackage } from "@embedpdf/plugin-viewport";
import { ScrollPluginPackage, ScrollStrategy } from "@embedpdf/plugin-scroll";
import { ZoomMode, ZoomPluginPackage } from "@embedpdf/plugin-zoom";
import { TilingPluginPackage } from "@embedpdf/plugin-tiling";
import { RotatePluginPackage } from "@embedpdf/plugin-rotate";
import { SpreadPluginPackage } from "@embedpdf/plugin-spread";
import { FullscreenPluginPackage } from "@embedpdf/plugin-fullscreen";
import { ExportPluginPackage } from "@embedpdf/plugin-export";
import { RenderPluginPackage } from "@embedpdf/plugin-render";
const plugins = [
createPluginRegistration(LoaderPluginPackage, {
loadingOptions: {
type: "url",
pdfFile: {
id: "1",
url: "https://snippet.embedpdf.com/ebook.pdf",
},
options: {
mode: "full-fetch",
},
},
}),
createPluginRegistration(ViewportPluginPackage, {
viewportGap: 10,
}),
createPluginRegistration(ScrollPluginPackage, {
strategy: ScrollStrategy.Vertical,
}),
createPluginRegistration(RenderPluginPackage),
createPluginRegistration(TilingPluginPackage, {
tileSize: 768,
overlapPx: 2.5,
extraRings: 0,
}),
createPluginRegistration(ZoomPluginPackage, {
defaultZoomLevel: ZoomMode.FitPage,
}),
createPluginRegistration(RotatePluginPackage),
createPluginRegistration(SpreadPluginPackage),
createPluginRegistration(FullscreenPluginPackage),
createPluginRegistration(ExportPluginPackage),
]
Great! Now let's set up the React components. First we have to import the React components for these plugins.
import { FilePicker } from "@embedpdf/plugin-loader/react";
import { Viewport } from "@embedpdf/plugin-viewport/react";
import { Scroller } from "@embedpdf/plugin-scroll/react";
import { TilingLayer } from "@embedpdf/plugin-tiling/react";
import { Rotate } from "@embedpdf/plugin-rotate/react";
import { FullscreenProvider } from "@embedpdf/plugin-fullscreen/react";
import { Download } from "@embedpdf/plugin-export/react";
import { RenderLayer } from "@embedpdf/plugin-render/react";
Now that is done we can setup the components as follows:
<EmbedPDF onInitialized={async () => {}} engine={engine} plugins={plugins}>
{({ pluginsReady }) => (
<FullscreenProvider>
<Viewport
style={{
width: "100%",
height: "100%",
flexGrow: 1,
backgroundColor: "#f1f3f5",
overflow: "auto",
}}
>
{pluginsReady ? (
<Scroller
renderPage={({ pageIndex, scale, width, height, document }) => (
<Rotate pageSize={{ width, height }}>
<Box
key={document?.id}
sx={{
width,
height,
position: "relative",
backgroundColor: "white",
}}
>
<RenderLayer pageIndex={pageIndex} />
<TilingLayer pageIndex={pageIndex} scale={scale} />
</Box>
</Rotate>
)}
/>
) : (
<Loading />
)}
</Viewport>
<Download />
<FilePicker />
</FullscreenProvider>
)}
</EmbedPDF>
Now we have a very basic headless PDF viewer! If everything went well it should look as the example below.
Now it's time to start adding the UI components. So that we are able to control the PDF viewer from a toolbar.
Adding the MUI components
Alright, here’s where the magic happens! We’ve got our headless PDF viewer set up, but it’s not much use without some controls. Let’s add a toolbar with MUI components to handle zooming, page navigation, rotating pages, changing layouts, and more. I’ve already included the code for the Toolbar, ZoomControls, PageControls, so let’s break down how they work and fit them into our viewer.
ToggleIconButton
The ToggleIconButton
is a custom MUI IconButton
that changes its look when a menu is open—like a visual cue for the user. It’s used in the Toolbar
and ZoomControls
.
import {
IconButtonProps as MuiIconButtonProps,
IconButton as MuiIconButton,
alpha,
} from "@mui/material";
interface ToggleIconButtonProps extends MuiIconButtonProps {
isOpen: boolean;
// Extend with MUI's IconButton props if needed
children: React.ReactNode;
}
export const ToggleIconButton: React.FC<ToggleIconButtonProps> = ({
isOpen,
children,
...props
}) => {
return (
<MuiIconButton
edge="start"
size={"small"}
aria-label="toggle search"
aria-pressed={isOpen}
{...props}
sx={{
bgcolor: isOpen
? (theme) => alpha(theme.palette.common.white, 0.24)
: "transparent",
"&:hover": {
bgcolor: (theme) => alpha(theme.palette.common.white, 0.16),
},
transition: "background-color 120ms",
color: "white",
...props.sx,
}}
>
{children}
</MuiIconButton>
);
};
ZoomControls
Next up, the ZoomControls
let you zoom in and out of the PDF. This component uses the useZoom
hook to get the current zoom level and control it. Here’s what you get:
- A little input field where you can type a custom zoom percentage (like 150%) and hit Enter to apply it.
- A dropdown menu with preset zoom levels (50%, 100%, 200%, etc.) and modes like “Fit to Page” or “Fit to Width”.
- Plus and minus buttons to zoom in or out step-by-step.
The input field syncs with the zoom level using some useState
and useEffect
magic, and the buttons call provides?.zoomIn()
or provides?.zoomOut()
. The dropdown uses provides?.requestZoom()
to jump to whatever you pick. Super handy for getting the perfect view!
import { useZoom } from "@embedpdf/plugin-zoom/react";
import {
IconButton,
Typography,
Box,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Divider,
Input,
} from "@mui/material";
import AddCircleOutlineOutlinedIcon from "@mui/icons-material/AddCircleOutlineOutlined";
import RemoveCircleOutlineOutlinedIcon from "@mui/icons-material/RemoveCircleOutlineOutlined";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import WidthNormalIcon from "@mui/icons-material/WidthNormal";
import WidthFullIcon from "@mui/icons-material/WidthFull";
import { useState, MouseEvent, useEffect, FormEvent } from "react";
import { ZoomLevel, ZoomMode } from "@embedpdf/plugin-zoom";
import { SvgIconTypeMap } from "@mui/material/SvgIcon";
import { OverridableComponent } from "@mui/material/OverridableComponent";
import { ToggleIconButton } from "./ToggleIconButton";
interface ZoomModeItem {
value: ZoomLevel;
label: string;
icon: OverridableComponent<SvgIconTypeMap<{}, "svg">> & {
muiName: string;
};
}
interface ZoomPresetItem {
value: number;
label: string;
}
const ZOOM_PRESETS: ZoomPresetItem[] = [
{ value: 0.5, label: "50%" },
{ value: 1, label: "100%" },
{ value: 1.5, label: "150%" },
{ value: 2, label: "200%" },
{ value: 4, label: "400%" },
{ value: 8, label: "800%" },
{ value: 16, label: "1600%" },
];
const ZOOM_MODES: ZoomModeItem[] = [
{ value: ZoomMode.FitPage, label: "Fit to Page", icon: WidthNormalIcon },
{ value: ZoomMode.FitWidth, label: "Fit to Width", icon: WidthFullIcon },
] as const;
export const ZoomControls = () => {
const { state, provides } = useZoom();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
// State for the controlled input
const [inputValue, setInputValue] = useState<string>(
Math.round(state.currentZoomLevel * 100).toString()
);
const open = Boolean(anchorEl);
// Effect to sync input value with external state changes (e.g., from buttons)
useEffect(() => {
const zoomPercentage = Math.round(state.currentZoomLevel * 100);
setInputValue(zoomPercentage.toString());
}, [state.currentZoomLevel]);
const handleClick = (event: MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleSelect = (value: ZoomLevel) => {
provides?.requestZoom(value);
handleClose();
};
const handleCustomZoomSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const newZoom = parseInt(inputValue, 10);
if (!isNaN(newZoom) && newZoom > 0) {
provides?.requestZoom(newZoom / 100);
}
};
return (
<Box
sx={{
backgroundColor: "rgba(255, 255, 255, 0.2)",
borderRadius: 1,
pl: 1,
pr: 0.3,
py: 0.3,
display: "flex",
alignItems: "center",
gap: 0.5,
}}
>
{/* Form to handle custom zoom submission on Enter */}
<Box
component="form"
onSubmit={handleCustomZoomSubmit}
sx={{ display: "flex", alignItems: "center" }}
>
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value.replace(/[^0-9]/g, ""))} // Allow only numbers
disableUnderline
inputProps={{
"aria-label": "Custom zoom percentage",
style: {
textAlign: "right",
padding: 0,
},
}}
sx={{
color: "white",
fontWeight: 500,
width: "35px", // Adjust width as needed
"& .MuiInputBase-input": {
fontFamily: "inherit",
fontSize: "0.875rem", // Corresponds to body2 variant
},
}}
/>
<Typography
variant="body2"
sx={{ color: "white", fontWeight: 500, mr: 0.5 }}
>
%
</Typography>
</Box>
<ToggleIconButton
isOpen={open}
sx={{ color: "white", p: 0.4 }}
onClick={handleClick}
>
<KeyboardArrowDownIcon fontSize="small" />
</ToggleIconButton>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
disablePortal
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
{/* Zoom Presets */}
{ZOOM_PRESETS.map(({ value, label }) => (
<MenuItem
key={value}
onClick={() => handleSelect(value)}
selected={Math.abs(state.currentZoomLevel - value) < 0.01}
>
{label}
</MenuItem>
))}
<Divider />
{/* Zoom Modes */}
{ZOOM_MODES.map(({ value, label, icon: Icon }) => (
<MenuItem
key={value}
onClick={() => handleSelect(value)}
selected={state.zoomLevel === value}
>
<ListItemIcon>
<Icon fontSize="small" />
</ListItemIcon>
<ListItemText>{label}</ListItemText>
</MenuItem>
))}
</Menu>
<IconButton
onClick={() => provides?.zoomOut()}
edge="start"
size="small"
sx={{ color: "white", p: 0.4 }}
aria-label="zoom out"
>
<RemoveCircleOutlineOutlinedIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => provides?.zoomIn()}
edge="start"
size="small"
sx={{ color: "white", p: 0.4 }}
aria-label="zoom in"
>
<AddCircleOutlineOutlinedIcon fontSize="small" />
</IconButton>
</Box>
);
};
PageControls
The PageControls
handle moving between pages. It uses the useScroll
hook to know the current page, total pages, and how to scroll to a specific page. You’ll see:
- Previous and next buttons to flip through pages one at a time.
- An input field where you can type a page number and hit Enter to jump right to it.
The buttons call scrollToPage?.({ pageNumber: currentPage - 1 })
or + 1
, and the input checks if the number you typed is valid before scrolling. It’s a simple way to navigate even a huge PDF.
import { Box, IconButton, TextField, Typography } from "@mui/material";
import { useScroll } from "@embedpdf/plugin-scroll/react";
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import NavigateBeforeIcon from "@mui/icons-material/NavigateBefore";
import { useEffect, useState } from "react";
export const PageControls = () => {
const { currentPage, totalPages, scrollToPage } = useScroll();
const [inputValue, setInputValue] = useState<string>(currentPage.toString());
useEffect(() => {
setInputValue(currentPage.toString());
}, [currentPage]);
const handlePageChange = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const pageStr = formData.get("page") as string;
const page = parseInt(pageStr);
if (!isNaN(page) && page >= 1 && page <= totalPages) {
scrollToPage?.({
pageNumber: page,
});
}
};
const handlePreviousPage = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.currentTarget.blur();
if (currentPage > 1) {
scrollToPage?.({
pageNumber: currentPage - 1,
});
}
};
const handleNextPage = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.currentTarget.blur();
if (currentPage < totalPages) {
scrollToPage?.({
pageNumber: currentPage + 1,
});
}
};
return (
<Box
sx={{
backgroundColor: "rgba(255, 255, 255, 0.2)",
borderRadius: 1,
pl: 1,
pr: 0.3,
py: 0.3,
display: "flex",
alignItems: "center",
gap: 0.5,
}}
>
<IconButton
onClick={handlePreviousPage}
edge="start"
size="small"
sx={{ color: "white", p: 0.4 }}
aria-label="zoom out"
>
<NavigateBeforeIcon fontSize="small" />
</IconButton>
<form
onSubmit={handlePageChange}
style={{ display: "flex", alignItems: "center", gap: 8 }}
>
<TextField
name="page"
value={inputValue}
onChange={(e) => {
const value = e.target.value.replace(/[^0-9]/g, "");
setInputValue(value);
}}
sx={{
// Target the root of the outlined input
"& .MuiOutlinedInput-root": {
// Style the border
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "rgba(255, 255, 255, 0.3)", // A slightly transparent white
},
// Style the border on hover
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "rgba(255, 255, 255, 0.7)",
},
// Style the border and boxShadow when focused
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "white",
},
},
}}
slotProps={{
input: {
inputProps: {
sx: {
width: "32px",
height: "20px",
padding: "4px",
textAlign: "center",
fontSize: "14px",
color: "white",
},
},
},
}}
variant="outlined"
size="small"
/>
<Typography variant="body2">{totalPages}</Typography>
</form>
<IconButton
onClick={handleNextPage}
edge="start"
size="small"
sx={{ color: "white", p: 0.4 }}
aria-label="zoom out"
>
<NavigateNextIcon fontSize="small" />
</IconButton>
</Box>
);
};
The Toolbar
The Toolbar is like the command center for our PDF viewer. It’s built with MUI’s AppBar and Toolbar components and sits at the top of the viewer. Inside, you’ll find:
- A menu button (with a little hamburger icon) that opens options like opening a file, downloading the PDF, or toggling fullscreen.
- A page settings button that lets you rotate pages or switch between page layouts (single, odd, or even pages).
- Our ZoomControls and PageControls, which we’ll get to in a sec.
To make it work, the Toolbar uses hooks from EmbedPDF like useRotateCapability
, useSpread
, useFullscreen
, useExportCapability
, and useLoaderCapability
. These hooks connect the buttons to the PDF viewer’s features. For example, clicking “Rotate Clockwise” calls rotateProvider?.rotateForward()
, and “Download” triggers exportProvider?.download()
. It’s all tied together with some React useState
to handle the menus opening and closing
import { useState } from "react";
import { useFullscreen } from "@embedpdf/plugin-fullscreen/react";
import { useExportCapability } from "@embedpdf/plugin-export/react";
import { useLoaderCapability } from "@embedpdf/plugin-loader/react";
import { useRotateCapability } from "@embedpdf/plugin-rotate/react";
import { useSpread } from "@embedpdf/plugin-spread/react";
import { SpreadMode } from "@embedpdf/plugin-spread";
import {
Box,
Divider,
AppBar,
Toolbar as MuiToolbar,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
ListSubheader,
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import RotateRightIcon from "@mui/icons-material/RotateRight";
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
import MenuBookOutlinedIcon from "@mui/icons-material/MenuBookOutlined";
import BookOutlinedIcon from "@mui/icons-material/BookOutlined";
import FullscreenOutlinedIcon from "@mui/icons-material/FullscreenOutlined";
import FullscreenExitOutlinedIcon from "@mui/icons-material/FullscreenExitOutlined";
import DownloadOutlinedIcon from "@mui/icons-material/DownloadOutlined";
import FileOpenOutlinedIcon from "@mui/icons-material/FileOpenOutlined";
import { ToggleIconButton } from "./ToggleIconButton";
import { PageSettingsIcon } from "./Icons";
import { ZoomControls } from "./ZoomControls";
import { PageControls } from "./PageControls";
export const Toolbar = () => {
const { provides: rotateProvider } = useRotateCapability();
const { spreadMode, provides: spreadProvider } = useSpread();
const { provides: fullscreenProvider, state: fullscreenState } =
useFullscreen();
const { provides: exportProvider } = useExportCapability();
const { provides: loaderProvider } = useLoaderCapability();
// Menu state for main menu
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const menuOpen = Boolean(menuAnchorEl);
// Menu state for page settings
const [pageSettingsAnchorEl, setPageSettingsAnchorEl] =
useState<null | HTMLElement>(null);
const pageSettingsOpen = Boolean(pageSettingsAnchorEl);
const handleMenuClick = (event: MouseEvent<HTMLElement>) => {
setMenuAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setMenuAnchorEl(null);
};
const handlePageSettingsClick = (event: MouseEvent<HTMLElement>) => {
setPageSettingsAnchorEl(event.currentTarget);
};
const handlePageSettingsClose = () => {
setPageSettingsAnchorEl(null);
};
const handleFullscreenToggle = () => {
fullscreenProvider?.toggleFullscreen();
handlePageSettingsClose();
handleMenuClose();
};
const handleDownload = () => {
exportProvider?.download();
handleMenuClose();
};
const handleOpenFilePicker = () => {
loaderProvider?.openFileDialog();
handleMenuClose();
};
const handleSpreadModeChange = (mode: SpreadMode) => {
spreadProvider?.setSpreadMode(mode);
handlePageSettingsClose();
};
const handleRotateForward = () => {
rotateProvider?.rotateForward();
setPageSettingsAnchorEl(null); // Close menu after action
};
const handleRotateBackward = () => {
rotateProvider?.rotateBackward();
setPageSettingsAnchorEl(null); // Close menu after action
};
return (
<AppBar position="static">
<MuiToolbar variant="dense" disableGutters sx={{ gap: 1.5, px: 1.5 }}>
<ToggleIconButton isOpen={menuOpen} onClick={handleMenuClick}>
<MenuIcon fontSize="small" />
</ToggleIconButton>
<Menu
anchorEl={menuAnchorEl}
open={menuOpen}
onClose={handleMenuClose}
disablePortal
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<MenuItem onClick={handleOpenFilePicker}>
<ListItemIcon>
<FileOpenOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Open File</ListItemText>
</MenuItem>
<MenuItem onClick={handleDownload}>
<ListItemIcon>
<DownloadOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Download</ListItemText>
</MenuItem>
<MenuItem onClick={handleFullscreenToggle}>
<ListItemIcon>
{fullscreenState.isFullscreen ? (
<FullscreenExitOutlinedIcon fontSize="small" />
) : (
<FullscreenOutlinedIcon fontSize="small" />
)}
</ListItemIcon>
<ListItemText>
{fullscreenState.isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
</ListItemText>
</MenuItem>
</Menu>
<Divider
orientation="vertical"
flexItem
sx={{ backgroundColor: "white", my: 1.2, opacity: 0.5 }}
/>
<ToggleIconButton
isOpen={pageSettingsOpen}
onClick={handlePageSettingsClick}
>
<PageSettingsIcon fontSize="small" />
</ToggleIconButton>
<Menu
anchorEl={pageSettingsAnchorEl}
open={pageSettingsOpen}
onClose={handlePageSettingsClose}
disablePortal
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<ListSubheader>Page Orientation</ListSubheader>
<MenuItem onClick={handleRotateForward}>
<ListItemIcon>
<RotateRightIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Rotate Clockwise</ListItemText>
</MenuItem>
<MenuItem onClick={handleRotateBackward}>
<ListItemIcon>
<RotateLeftIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Rotate Counter-clockwise</ListItemText>
</MenuItem>
<Divider />
<ListSubheader>Page Layout</ListSubheader>
<MenuItem
onClick={() => handleSpreadModeChange(SpreadMode.None)}
selected={spreadMode === SpreadMode.None}
>
<ListItemIcon>
<DescriptionOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Single Page</ListItemText>
</MenuItem>
<MenuItem
onClick={() => handleSpreadModeChange(SpreadMode.Odd)}
selected={spreadMode === SpreadMode.Odd}
>
<ListItemIcon>
<MenuBookOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Odd Pages</ListItemText>
</MenuItem>
<MenuItem
onClick={() => handleSpreadModeChange(SpreadMode.Even)}
selected={spreadMode === SpreadMode.Even}
>
<ListItemIcon>
<BookOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Even Pages</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={handleFullscreenToggle}>
<ListItemIcon>
{fullscreenState.isFullscreen ? (
<FullscreenExitOutlinedIcon fontSize="small" />
) : (
<FullscreenOutlinedIcon fontSize="small" />
)}
</ListItemIcon>
<ListItemText>
{fullscreenState.isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
</ListItemText>
</MenuItem>
</Menu>
<Divider
orientation="vertical"
flexItem
sx={{ backgroundColor: "white", my: 1.2, opacity: 0.5 }}
/>
<ZoomControls />
<Divider
orientation="vertical"
flexItem
sx={{ backgroundColor: "white", my: 1.2, opacity: 0.5 }}
/>
<PageControls />
</MuiToolbar>
</AppBar>
);
};
Putting It All Together
To hook these components into our viewer, we just need to tweak the PdfViewer
component a bit. Add the Toolbar
right above the Viewport
, and make sure it’s all wrapped nicely in the FullscreenProvider
. Here’s how it looks:
<EmbedPDF onInitialized={async () => {}} engine={engine} plugins={plugins}>
{({ pluginsReady }) => (
<FullscreenProvider>
<Box
sx={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
position: "relative",
userSelect: "none",
}}
>
<Toolbar />
<Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
<Viewport
style={{
width: "100%",
height: "100%",
flexGrow: 1,
backgroundColor: "#f1f3f5",
overflow: "auto",
}}
>
{pluginsReady ? (
<Scroller
renderPage={({ pageIndex, scale, width, height, document }) => (
<Rotate pageSize={{ width, height }}>
<Box
key={document?.id}
sx={{
width,
height,
position: "relative",
backgroundColor: "white",
}}
>
<RenderLayer pageIndex={pageIndex} />
<TilingLayer pageIndex={pageIndex} scale={scale} />
</Box>
</Rotate>
)}
/>
) : (
<Loading />
)}
</Viewport>
</Box>
</Box>
<Download />
<FilePicker />
</FullscreenProvider>
)}
</EmbedPDF>
Check out the final result in this sandbox:
Wrapping Up
There you go! With these MUI components, you’ve got a PDF viewer that’s not only functional but also looks right at home in your React MUI project. You can zoom, flip pages, rotate, switch layouts, and more—all with a clean, familiar interface. This setup saved me tons of time, and I hope it does the same for you. If you hit any snags, the Embed PDF website is a great place to dig deeper. Now, go build something awesome with your new PDF viewer!
Top comments (0)