Modular UI Customization Showcase Demo Code Sample

Requirements
View Demo

Easily customize the viewer UI through modular components defined in JSON. Each component can be specified independently, enabling dynamic and granular control over the interface layout and behavior.

This demo allows you to:

  • Select from sample JSON files to switch the UI components:
    • Default UI
    • Alternate UI
    • Vertical Headers
    • No Ribbons
    • Ribbons with Icons
  • Review modular components and their structure in JSON data files

Implementation steps
To add Modular UI Customization capability with WebViewer:

Step 1: Choose your preferred web stack
Step 2: Download required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided in this guide

Demo Dependencies
This sample uses the following:

Want to see a live version of this demo?

Try the Modular UI Customization demo

1
2// ES6 Compliant Syntax
3// GitHub Copilot - October 7, 2025
4// File: modular-ui-customization/index.js
5
6import WebViewer from '@pdftron/webviewer';
7import { saveAs } from 'file-saver';
8
9const element = document.getElementById('viewer');
10const onLoad = async (instance) => {
11 instance.UI.enableFeatureFlag(instance.UI.FeatureFlags.CUSTOMIZABLE_UI);
12
13 // Load default configuration
14 const defaultConfig = await loadConfiguration(0);
15 if (defaultConfig) {
16 configurationOptions[0].cachedConfig = defaultConfig;
17 instance.UI.importModularComponents(defaultConfig);
18 }
19};
20
21let theInstance = null;
22// Initialize WebViewer and load default document
23WebViewer(
24 {
25 path: '/lib',
26 licenseKey: 'YOUR_LICENSE_KEY',
27 initialDoc: 'https://apryse.s3.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf',
28 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
29 },
30 element
31).then((instance) => {
32 theInstance = instance;
33 onLoad(instance);
34});
35
36// Configuration metadata with file mapping
37const configurationOptions = [
38 {
39 title: 'Default UI', configFile: 'default.json', cachedConfig: null,
40 description: 'The default UI features our out-of-the-box ribbon interface created using Modular Components.',
41 },
42 {
43 title: 'Alternate UI', configFile: 'custom-left.json', cachedConfig: null,
44 description: 'This alternate UI replaces the left tab panel with individual toggle buttons to open standalone panels.\n' +
45 'A vertical header is added to the left side of the UI to house the panel toggles and the select/pan tools.',
46 },
47 {
48 title: 'Vertical Headers', configFile: 'vertical.json', cachedConfig: null,
49 description: 'This configuration features vertical headers on the left and right sides of the UI.\n' +
50 'The left header contains the icon ribbons tools and the right header contains panel toggles as well as page navigation controls.',
51 },
52 {
53 title: 'No Ribbons', configFile: 'no-ribbons.json', cachedConfig: null,
54 description: 'This configuration uses tools without ribbons, instead, the tools are placed in the top header.',
55 },
56 {
57 title: 'Ribbons with Icons', configFile: 'ribbons-with-icons.json', cachedConfig: null,
58 description: 'This configuration uses tools with ribbons and icons, as well as a right vertical header with panel toggles as well as page navigation controls.',
59 },
60 {
61 title: 'Custom Configuration', configFile: '', cachedConfig: null, description: null,
62 },
63];
64
65const customConfigIndex = configurationOptions.findIndex(config => config.title === 'Custom Configuration');
66
67// Function to load configuration file
68const loadConfiguration = async (configIndex) => {
69 const configFile = configurationOptions[configIndex].configFile;
70 if (configFile === '') return null;
71
72 try {
73 const response = await fetch(`/showcase-demos/modular-ui-customization/modular-ui/${configFile}`);
74 if (!response.ok) {
75 throw new Error(`Failed to load configuration: ${response.statusText}`);
76 }
77 return await response.json();
78 } catch (error) {
79 console.error('Error loading configuration:', error);
80 return null;
81 }
82};
83
84// UI section
85
86// Create a container for all controls (labels, radio buttons and button)
87const controlsContainer = document.createElement('div');
88
89// Create a radio group for configurations
90const configGroup = document.createElement('div');
91
92configurationOptions.forEach((config, index) => {
93 const label = document.createElement('label');
94 label.textContent = config.title;
95 label.htmlFor = config.title;
96 label.className = 'radio-label-class';
97
98 const radio = document.createElement('input');
99 radio.type = 'radio';
100 radio.name = 'config';
101 radio.id = config.title;
102 radio.value = config.configFile;
103 radio.checked = (index === 0); // Check the first option by default
104 radio.disabled = (index === customConfigIndex);
105 configGroup.appendChild(radio);
106 configGroup.appendChild(label);
107 radio.onchange = async () => {
108 descriptionElement.textContent = configurationOptions[index].description || '';
109 // If we already loaded this configuration, use the cached version
110 if (configurationOptions[index].cachedConfig) {
111 await theInstance.UI.importModularComponents(configurationOptions[index].cachedConfig);
112 return;
113 }
114 // Otherwise, load it from the file
115 const config = await loadConfiguration(index);
116 if (config) {
117 configurationOptions[index].cachedConfig = config;
118 await theInstance.UI.importModularComponents(config);
119 }
120 };
121});
122
123controlsContainer.appendChild(configGroup);
124// Create a description area
125const descriptionElement = document.createElement('div');
126descriptionElement.style.marginTop = '10px';
127descriptionElement.style.whiteSpace = 'pre-wrap';
128descriptionElement.textContent = configurationOptions[0].description;
129controlsContainer.appendChild(descriptionElement);
130
131// Create a hidden file input for uploading configuration files
132const fileUpload = document.createElement('input');
133fileUpload.style.display = 'none';
134fileUpload.type = 'file';
135fileUpload.accept = '.json';
136fileUpload.onchange = (event) => {
137 const file = event.target.files[0];
138 if (file) {
139 const reader = new FileReader();
140 reader.onload = async (e) => {
141 try {
142 const jsonContent = JSON.parse(e.target.result);
143 await theInstance.UI.importModularComponents(jsonContent);
144 descriptionElement.textContent = 'Custom configuration loaded from file: ' + file.name;
145 configurationOptions[customConfigIndex].cachedConfig = jsonContent;
146 configurationOptions[customConfigIndex].description = 'Cached custom configuration from file: ' + file.name;
147 const customRadio = document.getElementById('Custom Configuration');
148 customRadio.disabled = false;
149 customRadio.checked = true;
150 } catch (error) {
151 console.error('Invalid JSON file:', error);
152 descriptionElement.textContent = 'Error Loading Configuration, See Console For Details';
153 }
154 };
155 reader.readAsText(file);
156 }
157};
158
159const downloadJSON = (data) => {
160 // Convert JSON data to string
161 const jsonString = JSON.stringify(data, null, 2); // Pretty print with 2 spaces indentation
162
163 // Create a Blob from the JSON string
164 const blob = new Blob([jsonString], { type: 'application/json' });
165
166 // Use FileSaver's saveAs function to trigger the download
167 saveAs(blob, 'modular_components.json');
168};
169
170// Create a button to load the selected configuration
171const buttonLoad = document.createElement('button');
172buttonLoad.textContent = 'Load UI Configuration';
173buttonLoad.onclick = async () => {
174 fileUpload.click();
175}
176controlsContainer.appendChild(buttonLoad);
177// Create a button to download the current configuration
178const buttonDownload = document.createElement('button');
179buttonDownload.textContent = 'Download UI Configuration';
180buttonDownload.onclick = async () => {
181 try {
182 const data = theInstance.UI.exportModularComponents();
183 downloadJSON(data);
184 } catch (error) {
185 console.error('Error exporting modular components:', error);
186 }
187}
188controlsContainer.appendChild(buttonDownload);
189// Apply classes for styling using CSS
190buttonLoad.className = 'btn-style';
191buttonDownload.className = 'btn-style';
192controlsContainer.className = 'control-container';
193// Append elements to the controls container
194element.parentElement.insertBefore(controlsContainer, element);
195

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales