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: Get started with WebViewer in your preferred web stack
Step 2: Add the ES6 JavaScript sample code provided in this guide

Once you generate your license key, it will automatically be included in your sample code below.

License Key

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

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales