TL;DR: Learn how to implement role-based PDF annotation in React. This guide walks you through filtering annotations by user, locking non-author edits, and enabling secure collaborative workflows using a PDF viewer component.
Managing PDF annotations in team projects often gets messy, unstructured comments, unclear responsibilities, and security risks can derail collaboration. A role-based approach solves these challenges by linking each annotation to a specific user and assigning permissions based on roles, so you control who can add, view, or edit comments. This keeps feedback organized and prevents unauthorized changes.
In this guide, we’ll show you how to build a secure, role-based PDF annotation system in React to streamline collaboration and enhance team productivity.
Steps to integrate a Syncfusion PDF Viewer in React
To start adding annotations in React, you first need to set up the Syncfusion PDF Viewer in your application. Follow these steps:
Step 1: Initialize your React project
Start by creating a new React app using the Create React App command:
npx create-react-app syncfusion-pdf-viewer
cd syncfusion-pdf-viewer
Step 2: Install Syncfusion React PDF Viewer
Install the required Syncfusion React PDF Viewer library to bring all its capabilities into your React project. This package includes all the necessary modules for rendering PDFs:
npm install @syncfusion/ej2-react-pdfviewer
Step 3: Import required styles
Next, import the required CSS files into your PdfViewer.css to ensure proper styling:
@import '../node_modules/@syncfusion/ej2-base/styles/material.css';
@import '../node_modules/@syncfusion/ej2-buttons/styles/material.css';
@import '../node_modules/@syncfusion/ej2-dropdowns/styles/material.css';
@import '../node_modules/@syncfusion/ej2-inputs/styles/material.css';
@import '../node_modules/@syncfusion/ej2-navigations/styles/material.css';
@import '../node_modules/@syncfusion/ej2-popups/styles/material.css';
@import '../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css';
@import "../node_modules/@syncfusion/ej2-pdfviewer/styles/material.css";
Step 4. Add the Syncfusion React PDF Viewer
Then, add the Syncfusion PDF Viewer to your React app:
import './PdfViewer.css';
import {
PdfViewerComponent,
Toolbar,
Magnification,
Navigation,
LinkAnnotation,
BookmarkView,
ThumbnailView,
Print,
TextSelection,
Annotation,
TextSearch,
FormFields,
FormDesigner,
Inject,
} from '@syncfusion/ej2-react-pdfviewer';
function App() {
return (
<div>
<div className="control-section">
{/* Render the PDF Viewer */}
<PdfViewerComponent
id="container"
documentPath="https://cdn.syncfusion.com/content/pdf/pdf-succinctly.pdf"
resourceUrl="https://cdn.syncfusion.com/ej2/31.2.7/dist/ej2-pdfviewer-lib"
style={{ height: '640px' }}
>
<Inject
services={[
Toolbar,
Magnification,
Navigation,
Annotation,
LinkAnnotation,
BookmarkView,
ThumbnailView,
Print,
TextSelection,
TextSearch,
FormFields,
FormDesigner,
]}/>
</PdfViewerComponent>
</div>
</div>
);
}
Now that your React app is set up with Syncfusion PDF Viewer and user authentication, let’s move on to implementing role-based annotation controls to ensure secure and organized collaboration
User identification with Authentication
To keep annotations secure and personalized, start with a simple authentication flow. When users open the app, they’re greeted with a login page where they can sign in or create an account.
Once logged in, they access the PDF Viewer and work exclusively with their own annotations. Every comment is tied to their account, so users only see and edit their own notes, keeping feedback organized and accountability clear.
Whether you’re building a document review tool or a file-sharing platform, authentication ensures that annotations are user-specific, secure, and easy to manage.
// Authentication: Login/Register UI. Posts to /login or /register and calls onLogin(user) on success.
function Authentication({ onLogin }) {
const [isRegistering, setIsRegistering] = useState(false);
const [message, setMessage] = useState("");
const [username, setUsername] = useState("");
const validateEmail = (email) => /\S+@\S+\.\S+/.test(email);
const handleLogin = async (event) => {
event.preventDefault();
setMessage("");
const form = event.currentTarget;
const email = form.elements.email.value.trim();
const password = form.elements.password.value.trim();
if (!validateEmail(email)) {
setMessage(" Please enter a valid email.");
return;
}
try {
const response = await fetch(`${hostURL}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
const userData = await response.json();
localStorage.setItem("user", JSON.stringify(userData));
onLogin(userData);
} else {
setMessage(" Invalid email or password.");
}
} catch {
setMessage(" Server error during login.");
}
};
const handleRegister = async (e) => {
e.preventDefault();
setMessage("");
const form = e.currentTarget;
const email = form.elements.email.value;
const password = form.elements.password.value;
if (!validateEmail(email)) {
setMessage(" Please enter a valid email.");
return;
}
try {
const register = await fetch(`${hostURL}/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, email, password }),
});
const data = await register.json();
if (register.ok) {
setMessage(" Registration successful. You can now log in.");
setIsRegistering(false);
setUsername("");
} else {
setMessage(`${data.message || "Registration failed"}`);
}
} catch {
setMessage(" Server error during registration.");
}
};
return (
<div className="auth-container">
<h2 className="auth-title">{isRegistering ? "Register" : "Login"}</h2>
<form
className="auth-form"
onSubmit={isRegistering ? handleRegister : handleLogin}>
{isRegistering && (
<label className="auth-label" htmlFor="username">
Username:
</label>
<input
id="username"
type="text"
name="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="auth-input"
placeholder="Your username"/>
)}
<label className="auth-label" htmlFor="email">
Email:
</label>
<input
id="email"
type="email"
name="email"
required
className="auth-input"
placeholder="you@example.com"/>
<label className="auth-label" htmlFor="password">
Password:
</label>
<input
id="password"
type="password"
name="password"
required
className="auth-input"
placeholder="Your password"/>
<button type="submit" className="auth-button primary">
{isRegistering ? "Register" : "Login"}
</button>
</form>
<button
className="auth-button secondary"
onClick={() => {
setIsRegistering(!isRegistering);
setMessage("");
setUsername("");
}}
style={{ marginTop: 12 }}
>
{isRegistering
? "Already have an account? Log in"
: "Don't have an account? Register"}
</button>
{message && <p className="auth-message">{message}</p>}
</div>
);
}
export default Authentication;
With this code, your React app renders the Syncfusion PDF Viewer alongside a login page, enabling authenticated access to personalized annotation features for safe and collaborative document workflows.

Show or hide annotation based on the user
Controlling annotation visibility is key to creating a clean, personalized experience. With the Syncfusion PDF Viewer’s APIs, such as deleteAnnotations, importAnnotation, and exportAnnotationsAsObject, you can dynamically manage which comments appear for each user.
Here’s how it works: export all annotations, clear them from the view, and then re-import only those that match the logged-in user’s filter. This ensures every user sees only their own notes, keeping collaboration organized and secure.
By leveraging these APIs, you gain full control over annotation workflows, making document reviews more structured, user-specific, and scalable for team projects.
/**
* Filters annotations by user and imports them into the viewer.
* - If userName is 'All Authors', import everything.
* - Otherwise, import only annotations where title === userName.
*/
const filterAndLoadAnnotations = (userName, allAnnotations) => {
if (!pdfViewerRef.current) return;
setTimeout(() => {
try {
pdfViewerRef.current.deleteAnnotations();
} catch {}
if (!allAnnotations) return;
try {
if (userName === 'All Authors') {
let parsedData =
typeof allAnnotations === 'string' && allAnnotations.trim() !== ''
? JSON.parse(allAnnotations)
: allAnnotations;
if (!parsedData) return;
const dataToImport = parsedData.pdfAnnotation ? parsedData : { pdfAnnotation: parsedData };
pdfViewerRef.current.importAnnotation(dataToImport);
return;
}
let parsedData =
typeof allAnnotations === 'string' ? JSON.parse(allAnnotations) : allAnnotations;
if (!parsedData?.pdfAnnotation) parsedData = { pdfAnnotation: parsedData };
const filteredData = { pdfAnnotation: {} };
Object.keys(parsedData.pdfAnnotation).forEach(pageNumber => {
const pageData = parsedData.pdfAnnotation[pageNumber];
const list = pageData?.shapeAnnotation || [];
const userAnnotations = list.filter(a => a.title === userName);
if (userAnnotations.length > 0) {
filteredData.pdfAnnotation[pageNumber] = { shapeAnnotation: userAnnotations };
}
});
if (Object.keys(filteredData.pdfAnnotation).length > 0) {
pdfViewerRef.current.importAnnotation(filteredData);
}
} catch (error) {
console.error('Error filtering or loading annotations:', error);
}
}, 0);
};
View all annotations with “All Authors” mode
Need a complete picture of team feedback? The “All Authors” mode displays every annotation in the document, giving reviewers full context for collaborative decisions. This is ideal for team-wide reviews where seeing all comments and markups helps maintain clarity and transparency.

Filter annotations by selected user
Sometimes, you need focus. By selecting a specific user from the dropdown, the PDF Viewer filters annotations to show only that person’s comments. This creates a distraction-free review experience, perfect for checking individual feedback without the noise of other contributors.

Add annotations based on the logged-in user
The annotation toolbar is smart; it is enabled only when the dropdown is set to “All Authors” or the logged-in user. This prevents unauthorized edits and keeps annotation integrity intact. When a user adds a new note, the system automatically assigns their name as the author, ensuring accountability and proper tracking.
/**
* When a new annotation is added:
* - Stamp the author as the current displayName
* - Export annotations and collect the ones related to the new item
* - Merge them into the global store so filters remain in sync
*/
const onAnnotationAdd = async (args) => {
const viewer = pdfViewerRef.current;
const coll = viewer.annotationCollection || [];
const addedAnnotation = coll[coll.length - 1];
if (!addedAnnotation) return;
addedAnnotation.author = displayName;
viewer.annotation.editAnnotation(addedAnnotation);
const exportedData = await viewer.exportAnnotationsAsObject();
const allAnnnots = JSON.parse(exportedData).pdfAnnotation;
const exportData = [];
if (allAnnnots) {
const keys = Object.keys(allAnnnots);
for (let x = 0; x < keys.length; x++) {
const pageAnnots = allAnnnots[keys[x]].shapeAnnotation || [];
for (let y = 0; y < pageAnnots.length; y++) {
const pageAnnot = pageAnnots[y];
if (pageAnnot.name === args.annotationId || pageAnnot.inreplyto === args.annotationId) {
exportData.push(pageAnnot);
}
}
}
}
combineAnnotations(exportData);
};
function combineAnnotations(exportData) {
const existingData = JSON.parse(globalAnnotationsData);
const key = exportData[0].page;
if (existingData.pdfAnnotation[key]) {
if (Array.isArray(existingData.pdfAnnotation[key].shapeAnnotation)) {
for (let x = 0; x < exportData.length; x++) {
existingData.pdfAnnotation[key].shapeAnnotation.push(exportData[x]);
}
} else {
const keysLength = Object.keys(existingData.pdfAnnotation[key].shapeAnnotation).length;
for (let x = 0; x < exportData.length; x++) {
existingData.pdfAnnotation[key].shapeAnnotation[(keysLength + x).toString()] = exportData[x];
}
}
} else {
existingData.pdfAnnotation[key] = { shapeAnnotation: {} };
for (let x = 0; x < exportData.length; x++) {
existingData.pdfAnnotation[key].shapeAnnotation[x.toString()] = exportData[x];
}
}
localStorage.setItem('annot', JSON.stringify(existingData));
const combinedAnnotations = JSON.stringify(existingData);
setGlobalAnnotationsData(combinedAnnotations);
Save annotations with logged-in user as author
When the logged-in user adds a new annotation, it’s automatically saved with their username as the author. This ensures clear ownership, accountability, and a personalized experience for every contributor.

Disable annotation editing for other users
To maintain document integrity, editing is restricted to the original author. If a different user is selected from the dropdown, the “Add or Edit Annotation” option is disabled. This prevents unauthorized changes and accidental edits during collaborative reviews.

Edit annotations based on the logged-in user:
Every annotation is tagged with the logged-in user’s name for transparency. To enforce security, only the author can edit their annotations—others remain locked. This is achieved using the isLock property:
- If the logged-in user is the author, isLock is set to false, allowing edits.
- If not, isLock is set to true, keeping the annotation protected.
This approach ensures organized collaboration, proper tracking, and document integrity throughout multi-user reviews.
/**
* Lock/unlock annotations on import based on simple ownership rules:
* - Author == logged-in user AND selected author == logged-in user -> unlocked
* - Else lock the annotation
*/
function lockAnnnotations() {
var annotations = pdfViewerRef.current.annotationCollection;
for (let i = 0; i < annotations.length; i++) {
const annot = annotations[i];
if (annot.author === loggedInUser.username && selectedAuthor === loggedInUser.username) {
annot.annotationSettings.isLock = false;
} else if (annot.author === "Guest" && selectedAuthor === "All Authors") {
annot.annotationSettings.isLock = false;
} else {
annot.annotationSettings.isLock = true;
}
pdfViewerRef.current.annotation.editAnnotation(annotations[i]);
}
}

Key benefits of user-based annotation support
- Personalized annotation control: Every annotation is linked to the logged-in user, ensuring clear authorship and preventing unauthorized edits.
- Smart annotation filtering: Switch between viewing all annotations or just your own, reducing clutter and focusing on relevant feedback.
- Developer-friendly API integration: Syncfusion offers intuitive APIs to add, filter, lock, and manage annotations for flexible implementation.
- Flexible & scalable for modern applications: Works across React, Angular, Vue, and Blazor. It supports role-based access control and read-only modes, making it ideal for secure, scalable enterprise applications.
GitHub reference
To explore the full annotation implementation on GitHub and try it yourself.
Conclusion
Syncfusion’s React PDF Viewer is a powerful solution for building user-specific PDF annotation workflows in React. Unlike standard PDF editors, it provides developers fine-grained control over annotation of visibility and editing, ensuring that each user interacts only with their own content.
With features like author tagging, role-based access, and secure editing logic, you can easily implement functionality such as filtering annotations by author, locking annotations for non-authors, and managing dynamic annotation layers.
If you’re developing a document review tool, legal workflow, or any application that requires personalized PDF annotations in React, Syncfusion’s PDF Viewer delivers clarity, control, and accountability, without the clutter.
If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.
You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!
Related blogs
- Top 5 React PDF Viewers for Smooth Document Handling
- How to Fix Memory Leaks in JavaScript PDF Viewers: Best Practices and Debugging Tips
- Top Security Risks in JavaScript PDF Viewers (and How to Fix Them)
- Securely Load Encrypted PDFs in WPF PDF Viewer Using Credentials
This article was originally published at Syncfusion.com.
Top comments (0)