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.
To start adding annotations in React, you first need to set up the Syncfusion PDF Viewer in your application. Follow these steps:
Start by creating a new React app using the Create React App command:
npx create-react-app syncfusion-pdf-viewer
cd syncfusion-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 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"; 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.
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.
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);
}; 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.
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.
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); 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.
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.
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:
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]);
}
} To explore the full annotation implementation on GitHub and try it yourself.
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!